<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/" version="2.0">
  <!-- Source: https://www.dolthub.com/blog/rss.xml -->
  <channel>
    <title>DoltHub Blog - Latest Posts</title>
    <description>Blog for DoltHub, a website hosting databases made with Dolt, an open-source version-controlled SQL database with Git-like semantics.</description>
    <link>https://siftrss.com/f/Kkmm5PG8lw</link>
    <language>en-us</language>
    <lastBuildDate>Fri, 18 Sep 2026 22:48:55 GMT</lastBuildDate>
    <atom:link href="https://siftrss.com/f/Kkmm5PG8lw" rel="self" type="application/rss+xml"/>
    <item>
      <title>Home and Pricing Page Redesign</title>
      <link>https://dolthub.com/blog/2026-09-15-new-homepage-and-pricing/</link>
      <guid isPermaLink="true">https://dolthub.com/blog/2026-09-15-new-homepage-and-pricing/</guid>
      <description>DoltHub is the version-controlled database company, and our new homepage and pricing page reflect that shift.</description>
      <pubDate>Tue, 15 Sep 2026 00:00:00 GMT</pubDate>
      <content:encoded>&lt;p&gt;&lt;a href="https://www.dolthub.com/blog/2026-07-16-dolt-in-4-flavors/"&gt;Dolt now comes in four flavors&lt;/a&gt;: Dolt, Doltgres, DoltLite and Dumbo. Our redesigned homepage and pricing page reflect that expanded lineup. This article walks through the new designs and highlights some of the messages we’re trying to emphasize.&lt;/p&gt;
&lt;h1 id="homepage"&gt;Homepage&lt;a class="anchor-link" aria-label="Link to heading" href="#homepage"&gt;#&lt;/a&gt;&lt;/h1&gt;
&lt;p&gt;DoltHub started as &lt;a href="https://www.dolthub.com/blog/2019-10-09-where-is-the-data-catalog/"&gt;a data sharing company&lt;/a&gt;. We still think DoltHub is great for sharing data but our customers mostly want to run our databases in production instead. Our fastest-growing use case is serving as the backing store for systems where &lt;a href="https://www.dolthub.com/blog/2026-06-04-agentic-writes/"&gt;agents make writes&lt;/a&gt;. Our new homepage reflects that.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://www.dolthub.com/"&gt;&lt;img src="https://static.dolthub.com/blogimages/new-homepage.png/080e805b65e17d5be2c2c035d173885fa99a55e79f34ca4293a23bfeab69dd50.webp" alt="New Homepage"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;A few things to note about this design: the image of agents hanging out on the branches of a tree growing from a database-shaped pot is not new. But it’s one of my favorite images our designers have ever made. It stays. We also wanted to make sure people knew from the start that we are the version-controlled database company and that our databases are perfect for agents. The new above-the-fold language reflects this.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;AGENTS NEED VERSION CONTROL&lt;br&gt;
We Build Databases for Agents&lt;br&gt;
The only databases with version control&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;We also wanted to highlight our three non-alpha databases above the fold: Dolt, Doltgres, and DoltLite. Dumbo will be included once it sheds the alpha tag.&lt;/p&gt;
&lt;p&gt;When we only had Dolt, most new folks visiting the homepage would assume Dolt was a MySQL add-on. Now that Dolt comes in four flavors, it’s clearer from the start that our databases are built from the ground up on novel storage to provide Git-style features. The messaging no longer centers on MySQL.&lt;/p&gt;
&lt;p&gt;Clicking on a database in the first section takes you to a card explaining what that database is and when to use it. These cards are my favorite part of the homepage. I think they are the best distillation of what makes each Dolt flavor special.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://www.dolthub.com/#dolt"&gt;&lt;img src="https://static.dolthub.com/blogimages/new-databases-section.png/0a7e23f056812bb02b8efc2145dbd190a4857415da344fa886fe239fc1083fba.webp" alt="New Databases Section"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Also, while you’re down there, &lt;a href="https://github.com/dolthub/doltlite"&gt;throw my baby DoltLite a star&lt;/a&gt;. It’s getting mogged by Dolt and Doltgres.&lt;/p&gt;
&lt;h1 id="pricing"&gt;Pricing&lt;a class="anchor-link" aria-label="Link to heading" href="#pricing"&gt;#&lt;/a&gt;&lt;/h1&gt;
&lt;p&gt;We applied lessons from designing the database cards to &lt;a href="https://www.dolthub.com/pricing"&gt;the new pricing page&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://www.dolthub.com/pricing"&gt;&lt;img src="https://static.dolthub.com/blogimages/new-pricing-page.png/40e5fe503e054b42f4681d01640204097e474f5c9247b8437a84a9c958d3cdd0.webp" alt="New Pricing Page"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;All of our databases are free and open source. We wanted to make that clear from the pricing page.&lt;/p&gt;
&lt;p&gt;We make money in two ways:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Enterprise Support&lt;/li&gt;
&lt;li&gt;Services&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The pricing page explains our services and links to our &lt;a href="https://www.dolthub.com/support"&gt;Enterprise Support page&lt;/a&gt;. I have a few ideas about how we could make paid support a clearer option so come back and refresh the pricing page every once in a while if you are curious.&lt;/p&gt;
&lt;h1 id="conclusion"&gt;Conclusion&lt;a class="anchor-link" aria-label="Link to heading" href="#conclusion"&gt;#&lt;/a&gt;&lt;/h1&gt;
&lt;p&gt;How do you like the new homepage and pricing page? Did we accomplish our design goals? Come by &lt;a href="https://discord.gg/gqr7K4VNKe"&gt;our Discord&lt;/a&gt; and give us your feedback.&lt;/p&gt;</content:encoded>
      <dc:creator>Tim Sehn</dc:creator>
      <category>dolthub</category>
    </item>
    <item>
      <title>Postgres follows the SQL standard for UPDATE statements, unlike MySQL</title>
      <link>https://dolthub.com/blog/2026-09-11-postgres-update-statements/</link>
      <guid isPermaLink="true">https://dolthub.com/blog/2026-09-11-postgres-update-statements/</guid>
      <description>Learn how Dolt and Doltgres support different SQL behavior on a shared engine using a new engine extension point</description>
      <pubDate>Fri, 11 Sep 2026 00:00:00 GMT</pubDate>
      <content:encoded>&lt;p&gt;&lt;a href="https://doltgres.com"&gt;Doltgres&lt;/a&gt;, the world’s first version-controlled Postgres-compatible database,
just &lt;a href="https://www.dolthub.com/blog/2026-08-06-doltgres-1-0/"&gt;hit 1.0&lt;/a&gt;, meaning that it’s ready for
production use. We want Doltgres to be a drop-in replacement for Postgres so that customers can use
the entire ecosystem of Postgres-compatible tools and libraries, or port their existing database
application to Doltgres without changing any code. This means getting all the nuanced semantics of
Postgres’s behavior correct in our emulation. And we think we’ve done pretty well here — our
compatibility tests &lt;a href="https://www.doltgres.com/docs/reference/supported-clients/clients/"&gt;encompass over two dozen tools and
languages&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;But Doltgres shares the same SQL engine Dolt uses, which was built to emulate MySQL semantics. For
most queries this works fine, but MySQL plays famously fast and loose with the SQL standard, while
Postgres takes it much more seriously. And because we take client compatibility very, very
seriously, that means that we need an engine that &lt;a href="https://www.dolthub.com/blog/2021-06-21-copying-mysqls-dumb-decisions/"&gt;reproduces all of MySQL’s wacky non-standard
behavior&lt;/a&gt; for Dolt and
Postgres’s dignified, correct behavior for Doltgres.&lt;/p&gt;
&lt;p&gt;Today’s blog is a case study of one area where the engine’s behavior differs to match the emulation
target, and a look under the hood for how we manage these differences internally in our interfaces.&lt;/p&gt;
&lt;h1 id="update-with-column-values-from-the-same-row"&gt;UPDATE with column values from the same row&lt;a class="anchor-link" aria-label="Link to heading" href="#update-with-column-values-from-the-same-row"&gt;#&lt;/a&gt;&lt;/h1&gt;
&lt;p&gt;&lt;a href="https://github.com/dolthub/doltgresql/issues/3092"&gt;This issue&lt;/a&gt; was brought to our attention by an
early adopter customer: Doltgres had the wrong behavior when an &lt;code&gt;UPDATE&lt;/code&gt; statement referenced table
columns in its update expressions.&lt;/p&gt;
&lt;pre class="astro-code github-dark" tabindex="0" data-language="sql"&gt;&lt;code&gt;&lt;span class="line"&gt;&lt;span&gt;CREATE&lt;/span&gt;&lt;span&gt; TABLE&lt;/span&gt;&lt;span&gt; t_seq&lt;/span&gt;&lt;span&gt; (a &lt;/span&gt;&lt;span&gt;int&lt;/span&gt;&lt;span&gt;, b &lt;/span&gt;&lt;span&gt;int&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;INSERT INTO&lt;/span&gt;&lt;span&gt; t_seq &lt;/span&gt;&lt;span&gt;VALUES&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;UPDATE&lt;/span&gt;&lt;span&gt; t_seq &lt;/span&gt;&lt;span&gt;SET&lt;/span&gt;&lt;span&gt; a &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; 2&lt;/span&gt;&lt;span&gt;, b &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; CASE&lt;/span&gt;&lt;span&gt; WHEN&lt;/span&gt;&lt;span&gt; a &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; 1&lt;/span&gt;&lt;span&gt; THEN&lt;/span&gt;&lt;span&gt; 100&lt;/span&gt;&lt;span&gt; ELSE&lt;/span&gt;&lt;span&gt; -&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt; END&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;SELECT&lt;/span&gt;&lt;span&gt; a, b &lt;/span&gt;&lt;span&gt;FROM&lt;/span&gt;&lt;span&gt; t_seq;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The SQL standard says that an &lt;code&gt;UPDATE&lt;/code&gt; statement that references column values should use the value
from the pre-update row, in all cases. So the &lt;code&gt;SELECT&lt;/code&gt; query in the above block should return this:&lt;/p&gt;
&lt;pre class="astro-code github-dark" tabindex="0" data-language="sql"&gt;&lt;code&gt;&lt;span class="line"&gt;&lt;span&gt; a |  b&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;---+-----&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt; 2&lt;/span&gt;&lt;span&gt; | &lt;/span&gt;&lt;span&gt;100&lt;/span&gt;&lt;span&gt;    -- per the SQL standard, every assignment reads the pre-update row&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;But MySQL doesn’t behave this way for an &lt;code&gt;UPDATE&lt;/code&gt;. It ignores the SQL standard and uses the new,
updated column values in every &lt;code&gt;UPDATE&lt;/code&gt; expression as it executes them one by one, left to right, on
each row. So in MySQL, and Dolt, the above select returns this:&lt;/p&gt;
&lt;pre class="astro-code github-dark" tabindex="0" data-language="sql"&gt;&lt;code&gt;&lt;span class="line"&gt;&lt;span&gt; a |  b&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;---+-----&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt; 2&lt;/span&gt;&lt;span&gt; |  &lt;/span&gt;&lt;span&gt;-&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;    -- the CASE saw the NEW value of a (=2)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And until earlier this week, Doltgres behaved this way too. But that’s wrong, and breaks client
expectations for Postgres application developers. We needed to change this behavior in the engine,
but only when running in Postgres emulation mode.&lt;/p&gt;
&lt;p&gt;How do we do that?&lt;/p&gt;
&lt;h1 id="introducing-engine-overrides"&gt;Introducing engine overrides&lt;a class="anchor-link" aria-label="Link to heading" href="#introducing-engine-overrides"&gt;#&lt;/a&gt;&lt;/h1&gt;
&lt;p&gt;During development of Doltgres, we experimented with a lot of different mechanisms to vary the
engine’s behavior for Doltgres, either to reflect needed differences for Postgres compatibility or
to implement features that MySQL doesn’t have. These include new rules during query analysis, new
plan nodes that wrap or otherwise alter existing ones, as well as more hacky fixes like swapping
function pointers during program init. For something like this divergence in behavior, there wasn’t
an existing extension point in the query engine. We &lt;a href="https://github.com/dolthub/go-mysql-server/blob/main/ARCHITECTURE.md"&gt;designed the
engine&lt;/a&gt; to make the database
backend swappable, as well as some of the query planning logic. But for something as fundamental as
applying updates to a row, we had not bothered to make the behavior pluggable.&lt;/p&gt;
&lt;p&gt;Our current approach in this kind of situation is to provide the engine with a set of well-defined
behavioral extension points at construction. Unlike the interfaces that define tables, databases,
functions, etc. that allow integrators to implement a custom database storage backend, these
extension points alter the query-time behavior of the engine itself, independent of the storage
backend. They’re currently stored in a struct called &lt;code&gt;EngineOverrides&lt;/code&gt;. To solve this particular
problem, we introduced the new &lt;code&gt;UpdateExpressionApplier&lt;/code&gt; interface at the bottom of the struct.&lt;/p&gt;
&lt;pre class="astro-code github-dark" tabindex="0" data-language="go"&gt;&lt;code&gt;&lt;span class="line"&gt;&lt;span&gt;type&lt;/span&gt;&lt;span&gt; EngineOverrides&lt;/span&gt;&lt;span&gt; struct&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;	// Builder contains functions and variables that can replace, supplement, or override functionality within the builder.&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;	Builder &lt;/span&gt;&lt;span&gt;BuilderOverrides&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;	// SchemaFormatter is the formatter for schema string creation. If nil, this will format in MySQL's style.&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;	SchemaFormatter &lt;/span&gt;&lt;span&gt;SchemaFormatter&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;	// Hooks contain various hooks that are called within a statement's lifecycle.&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;	Hooks &lt;/span&gt;&lt;span&gt;ExecutionHooks&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;	// CostedIndexScanExpressionFilter is used to walk expression trees in order to apply index scans based on&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;	// filter expressions. Some expressions may need to be modified or skipped in order to properly apply indexes&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;	// for all integrators.&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;	CostedIndexScanExpressionFilter &lt;/span&gt;&lt;span&gt;ExpressionTreeFilter&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;	// UpdateExpressionApplier evaluates UPDATE assignments. If nil, the engine uses&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;	// MySQL's sequential assignment evaluation and IGNORE conversion handling.&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;	UpdateExpressionApplier &lt;/span&gt;&lt;span&gt;UpdateExpressionApplier&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The new interface looks like this:&lt;/p&gt;
&lt;pre class="astro-code github-dark" tabindex="0" data-language="go"&gt;&lt;code&gt;&lt;span class="line"&gt;&lt;span&gt;// UpdateExpressionApplier evaluates the assignments for a row in an UPDATE statement.&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;// It does not apply to procedural SET or INSERT ON DUPLICATE KEY UPDATE statements.&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;type&lt;/span&gt;&lt;span&gt; UpdateExpressionApplier&lt;/span&gt;&lt;span&gt; interface&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;	ApplyRowUpdate&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;ctx&lt;/span&gt;&lt;span&gt; *&lt;/span&gt;&lt;span&gt;Context&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;updateExprs&lt;/span&gt;&lt;span&gt; *&lt;/span&gt;&lt;span&gt;UpdateExprs&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;tableSchema&lt;/span&gt;&lt;span&gt; Schema&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;oldRow&lt;/span&gt;&lt;span&gt; Row&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;ignore&lt;/span&gt;&lt;span&gt; bool&lt;/span&gt;&lt;span&gt;) (&lt;/span&gt;&lt;span&gt;Row&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;error&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For MySQL behavior, we have a simple interface that applies updates the same way it always has
(matching MySQL, not Postgres). For Doltgres, we implemented a new one that we plug in at engine
construction time.&lt;/p&gt;
&lt;pre class="astro-code github-dark" tabindex="0" data-language="go"&gt;&lt;code&gt;&lt;span class="line"&gt;&lt;span&gt;func&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;UpdateExpressionApplier&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;ApplyRowUpdate&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;ctx&lt;/span&gt;&lt;span&gt; *&lt;/span&gt;&lt;span&gt;sql&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Context&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;updateExprs&lt;/span&gt;&lt;span&gt; *&lt;/span&gt;&lt;span&gt;sql&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;UpdateExprs&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;tableSchema&lt;/span&gt;&lt;span&gt; sql&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Schema&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;oldRow&lt;/span&gt;&lt;span&gt; sql&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Row&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;_&lt;/span&gt;&lt;span&gt; bool&lt;/span&gt;&lt;span&gt;) (&lt;/span&gt;&lt;span&gt;sql&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Row&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;error&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;	newRow &lt;/span&gt;&lt;span&gt;:=&lt;/span&gt;&lt;span&gt; oldRow.&lt;/span&gt;&lt;span&gt;Copy&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;	for&lt;/span&gt;&lt;span&gt; _, expr &lt;/span&gt;&lt;span&gt;:=&lt;/span&gt;&lt;span&gt; range&lt;/span&gt;&lt;span&gt; updateExprs.&lt;/span&gt;&lt;span&gt;ExplicitUpdateExprs&lt;/span&gt;&lt;span&gt;() {&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;		assignment, ok &lt;/span&gt;&lt;span&gt;:=&lt;/span&gt;&lt;span&gt; expr.(&lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;span&gt;gmsexpression&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;SetField&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;		if&lt;/span&gt;&lt;span&gt; !&lt;/span&gt;&lt;span&gt;ok {&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;			return&lt;/span&gt;&lt;span&gt; nil&lt;/span&gt;&lt;span&gt;, fmt.&lt;/span&gt;&lt;span&gt;Errorf&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;"UPDATE: expected SetField, found &lt;/span&gt;&lt;span&gt;%T&lt;/span&gt;&lt;span&gt;"&lt;/span&gt;&lt;span&gt;, expr)&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;		}&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;		// SetField performs assignment conversion and returns a copy of oldRow.&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;		// Merge only its target, so later assignments cannot undo earlier writes.&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;		value, err &lt;/span&gt;&lt;span&gt;:=&lt;/span&gt;&lt;span&gt; assignment.&lt;/span&gt;&lt;span&gt;Eval&lt;/span&gt;&lt;span&gt;(ctx, oldRow)&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;		if&lt;/span&gt;&lt;span&gt; err &lt;/span&gt;&lt;span&gt;!=&lt;/span&gt;&lt;span&gt; nil&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;			return&lt;/span&gt;&lt;span&gt; nil&lt;/span&gt;&lt;span&gt;, err&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;		}&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;		...&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now Doltgres returns the expected result, the same as Postgres.&lt;/p&gt;
&lt;pre class="astro-code github-dark" tabindex="0" data-language="sql"&gt;&lt;code&gt;&lt;span class="line"&gt;&lt;span&gt;a |  b&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;---+-----&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;2&lt;/span&gt;&lt;span&gt; | &lt;/span&gt;&lt;span&gt;100&lt;/span&gt;&lt;span&gt;    -- per the SQL standard, every assignment reads the pre-update row&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Check out these &lt;a href="https://github.com/dolthub/go-mysql-server/pull/3840"&gt;two&lt;/a&gt;
&lt;a href="https://github.com/dolthub/doltgresql/pull/3300"&gt;PRs&lt;/a&gt; for the full details.&lt;/p&gt;
&lt;h1 id="conclusion"&gt;Conclusion&lt;a class="anchor-link" aria-label="Link to heading" href="#conclusion"&gt;#&lt;/a&gt;&lt;/h1&gt;
&lt;p&gt;Doltgres 1.0 already launched, but Doltgres’s compatibility story is definitely not over. Keep the
issues coming and &lt;a href="https://www.dolthub.com/blog/2024-05-15-24-hour-bug-fixes/"&gt;we’ll keep knocking them down in 24
hours&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Have a divergence in Postgres behavior to report? Want to learn more about Doltgres? Visit us on the
&lt;a href="https://discord.gg/gqr7K4VNKe"&gt;DoltHub Discord&lt;/a&gt; where our engineering team hangs out all day. Hope
to see you there.&lt;/p&gt;</content:encoded>
      <dc:creator>Zach Musgrave</dc:creator>
      <category>doltgres</category>
    </item>
    <item>
      <title>Implementing Type Contracts via Mutually Referencing Type Parameters</title>
      <link>https://dolthub.com/blog/2026-08-28-mutually-referencing-type-parameters/</link>
      <guid isPermaLink="true">https://dolthub.com/blog/2026-08-28-mutually-referencing-type-parameters/</guid>
      <description>It's not obvious how to properly write generic code that operates on recursive or mutually-recursive types. Here's how to do it right.</description>
      <pubDate>Fri, 28 Aug 2026 00:00:00 GMT</pubDate>
      <content:encoded>&lt;p&gt;I work for &lt;a href="https://www.dolthub.com/"&gt;Dolt&lt;/a&gt;, the world’s first version-controlled database. We made Dolt as a drop-in replacement for MySQL, since MySQL was the most commonly used SQL database in production when we started. But new teams by-and-large are not choosing MySQL; they’re using Postgres. So we also made &lt;a href="https://www.doltgres.com/"&gt;Doltgres&lt;/a&gt;, a alternate version of Dolt that speaks the Postgres dialect.&lt;/p&gt;
&lt;p&gt;Unsurprisingly, Dolt and Doltgres share a lot of common code. But there’s also subtle differences between MySQL and Postgres’s feature set. We want to use shared code for things that the projects have in common, and interfaces to implement behavior where they differ.&lt;/p&gt;
&lt;p&gt;One of our big features is Git-style branches. A client session operates on a currently checked-out branch, and is completely independent of the other branches in the database. Except when I say “completely independent”, I actually mean “mostly independent.” When we made Dolt, we decided that there was one specific situation where we wanted branches to &lt;em&gt;not&lt;/em&gt; be independent: auto incrementing columns.&lt;/p&gt;
&lt;p&gt;In MySQL, if a table has a column declared with the &lt;code&gt;AUTO_INCREMENT&lt;/code&gt; modifier, inserts aren’t required to specify a value for that column. Instead, a new value will be generated that is guaranteed to not conflict with any values currently in the table. This is true even when multiple sessions are making transactions concurrently, which each transaction getting a different value for the column. While transactions are typically independent from each other, MySQL makes an exception here so that the transactions won’t conflict if both are committed.&lt;/p&gt;
&lt;p&gt;We decided that it made sense to apply that same behavior to branches: if sessions on two different branches are inserting into the same table with an auto increment column, Dolt is guaranteed to generate non-conflicting values for that column. If we didn’t do this, then any attempt to merge those branches would result in a merge conflict.&lt;/p&gt;
&lt;p&gt;In contrast, Postgres doesn’t have &lt;code&gt;AUTO INCREMENT&lt;/code&gt;. Instead, it has &lt;code&gt;SERIAL&lt;/code&gt; columns, which behave similarly but have some key differences:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;In MySQL, AUTO INCREMENT values are always unsigned, while in Postgres, SERIAL values are always signed.&lt;/li&gt;
&lt;li&gt;SERIAL columns are backed by a data type called a Sequence, which has configuration parameters such as the min and max values, whether the generated values are incremented or decremented, whether they wrap around when they reach the end, etc.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Synchronizing the behavior across all branches and all transactions has a lot of tricky corner cases, that we’d already gotten right with AUTO INCREMENT, and duplicating that logic would be a bad idea. So we designed a data model that could handle both:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A &lt;code&gt;Sequence&lt;/code&gt; is an item in the database capable of producing a sequence of values, such as a MySQL table with an AUTO INCREMENT column, or a Postgres sequence.&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;SequenceValue&lt;/code&gt; is a value produced by a sequence.&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;SequenceState&lt;/code&gt; is a type that represents the current state of a &lt;code&gt;Sequence&lt;/code&gt; and can be incremented to produce new &lt;code&gt;SequenceValue&lt;/code&gt;s.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Using these definitions, we were able to implement all of the behavior common to both MySQL tables and Postgres sequences as generic methods on these types.&lt;/p&gt;
&lt;p&gt;This reminded me of a similar situation I encountered two years ago, involving pairs of data structures each consisting of a mutable type and an immutable type that could be converted between each other. The goal was to write something like the below, that could be used with any pair of types that satisfied this contract:&lt;/p&gt;
&lt;pre class="astro-code github-dark" tabindex="0" data-language="go"&gt;&lt;code&gt;&lt;span class="line"&gt;&lt;span&gt;func&lt;/span&gt;&lt;span&gt; ApplyMutations&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;immutable&lt;/span&gt;&lt;span&gt; ImmutableValue&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;mutations&lt;/span&gt;&lt;span&gt; []&lt;/span&gt;&lt;span&gt;Mutation&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;ImmutableValue&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;  mutableValue &lt;/span&gt;&lt;span&gt;:=&lt;/span&gt;&lt;span&gt; immutable.&lt;/span&gt;&lt;span&gt;Mutate&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;  mutableValue.&lt;/span&gt;&lt;span&gt;ApplyMutations&lt;/span&gt;&lt;span&gt;(mutations)&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;  return&lt;/span&gt;&lt;span&gt; mutableValue.&lt;/span&gt;&lt;span&gt;Flush&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I wrote a blog post talking about my solution to that problem, how I’d used a Golang generic interface to model a situation where a group of types collectively implement some contract. But there’s a lot in that I got flat-out wrong about how go Golang generics worked, and I got rightfully chewed out for it in the responses to the extent that I’m a bit embarrassed to bring it up again.&lt;sup&gt;&lt;a href="#user-content-fn-1" id="user-content-fnref-1" data-footnote-ref="" aria-describedby="footnote-label"&gt;1&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p&gt;This time, I was determined to make a better solution. This is a real problem, and it’s worth understanding the correct way to tackle it in Go because it’s a useful design pattern for writing type-safe code.&lt;/p&gt;
&lt;h1 id="the-problem"&gt;The Problem&lt;a class="anchor-link" aria-label="Link to heading" href="#the-problem"&gt;#&lt;/a&gt;&lt;/h1&gt;
&lt;p&gt;In Go, if you want a function to operate on multiple different types, you can define an interface. An interface is a contract: it specifies a set of constraints that the implementing type must satisfy, usually methods that the type must implement. Any type that implements these methods will satisfy the interface. But this interface only constrains a single type. Sometimes, you have a group of types that need to implement a contract together. How can we use Go’s language features to solve this problem?&lt;/p&gt;
&lt;h2 id="what-doesnt-work-interfaces"&gt;What Doesn’t Work: Interfaces&lt;a class="anchor-link" aria-label="Link to heading" href="#what-doesnt-work-interfaces"&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;If we were to express the above-mentioned Sequence data model as interfaces, it might look something like this:&lt;/p&gt;
&lt;pre class="astro-code github-dark" tabindex="0" data-language="go"&gt;&lt;code&gt;&lt;span class="line"&gt;&lt;span&gt;type&lt;/span&gt;&lt;span&gt; SequenceValue&lt;/span&gt;&lt;span&gt; interface&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;    int64&lt;/span&gt;&lt;span&gt; |&lt;/span&gt;&lt;span&gt; uint64&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;type&lt;/span&gt;&lt;span&gt; SequenceState&lt;/span&gt;&lt;span&gt; interface&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;    CurrentValue&lt;/span&gt;&lt;span&gt;() &lt;/span&gt;&lt;span&gt;SequenceValue&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;    Advance&lt;/span&gt;&lt;span&gt;() (&lt;/span&gt;&lt;span&gt;nextValue&lt;/span&gt;&lt;span&gt; SequenceValue&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;nextState&lt;/span&gt;&lt;span&gt; SequenceState&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;type&lt;/span&gt;&lt;span&gt; Sequence&lt;/span&gt;&lt;span&gt; interface&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;    CurrentState&lt;/span&gt;&lt;span&gt;() &lt;/span&gt;&lt;span&gt;SequenceState&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;    UpdateFromGlobalState&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;SequenceState&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;Sequence&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;But making these types regular interfaces is a bad idea for several reasons:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;There’s a performance penalty for calling interface methods due to dynamic dispatch.&lt;/li&gt;
&lt;li&gt;All values of an interface type are boxed and their underlying values are stored on the heap.&lt;/li&gt;
&lt;li&gt;Go doesn’t actually allow interfaces with shape constraints (like &lt;code&gt;SequenceValue&lt;/code&gt; above) to be used in method signatures.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;But the biggest problem is that this fails to give us the compile-time type safety we want. It doesn’t document or enforce that a particular &lt;code&gt;SequenceState&lt;/code&gt; implementation is always expected to have a specific type for &lt;code&gt;SequenceValue&lt;/code&gt;. We could need to insert check-casts every time one of these methods is called. If we ever call a method with the wrong implementation, we would panic at runtime.&lt;/p&gt;
&lt;p&gt;This isn’t a good use of interfaces, because we’re paying a cost in performance and code complexity but not getting anything out of it.&lt;/p&gt;
&lt;h2 id="the-bad-idea-a-single-generic-interface"&gt;The Bad Idea: A Single Generic Interface&lt;a class="anchor-link" aria-label="Link to heading" href="#the-bad-idea-a-single-generic-interface"&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;This was the concept in my previous attempt that rightly got a lot of pushback.&lt;/p&gt;
&lt;p&gt;The basic idea was that if interfaces are how you achieve polymorphism in Go, and an interface is defined by a set of behaviors on a single type, then you can achieve a contract on multiplace types by implementing an interface that accepts each of those types as a generic type parameter.&lt;/p&gt;
&lt;p&gt;So in this case, you would have a “contract” type that defines all the necessary behavior, and generic code must call methods on this contract type:&lt;/p&gt;
&lt;pre class="astro-code github-dark" tabindex="0" data-language="go"&gt;&lt;code&gt;&lt;span class="line"&gt;&lt;span&gt;type&lt;/span&gt;&lt;span&gt; SequenceContract&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;SequenceValue&lt;/span&gt;&lt;span&gt; any&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;SequenceState&lt;/span&gt;&lt;span&gt; any&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;Sequence&lt;/span&gt;&lt;span&gt; any&lt;/span&gt;&lt;span&gt;] &lt;/span&gt;&lt;span&gt;interface&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;    SequenceState_CurrentValue&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;SequenceState&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;SequenceValue&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;    SequenceState_Advance&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;SequenceState&lt;/span&gt;&lt;span&gt;) (&lt;/span&gt;&lt;span&gt;nextValue&lt;/span&gt;&lt;span&gt; SequenceValue&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;nextState&lt;/span&gt;&lt;span&gt; SequenceState&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;    Sequence_CurrentState&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;Sequence&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;SequenceState&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;    Sequence_UpdateFromGlobalState&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;Sequence&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;SequenceState&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;Sequence&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The main downside of this is that it’s ugly. Shared code can’t call methods on the component types because they all have an &lt;code&gt;any&lt;/code&gt; type constraint. Instead it &lt;em&gt;must&lt;/em&gt; call the methods on &lt;code&gt;SequenceContract&lt;/code&gt;, which will likely just delegate to the corresponding methods on the component types.&lt;/p&gt;
&lt;p&gt;This approach also requires that shared code takes a &lt;code&gt;SequenceContract&lt;/code&gt; value as an extra parameter. This also requires the shared code to be itself generic, which means that prior to Go 1.27 it couldn’t be a method and had to be a function.&lt;/p&gt;
&lt;p&gt;There are some possible upsides to this approach: implementations of the contract type are allowed to have state, and it’s possible to define multiple contracts on the same collection of types. But these “upsides” are a double-edged sword, because now you’re adding further complexity to your data model. This is rarely the best approach.&lt;/p&gt;
&lt;h1 id="almost-a-solution-generic-interfaces"&gt;Almost A Solution: Generic Interfaces&lt;a class="anchor-link" aria-label="Link to heading" href="#almost-a-solution-generic-interfaces"&gt;#&lt;/a&gt;&lt;/h1&gt;
&lt;p&gt;We could attempt to capture the relationship between types via generic interfaces. Going back to original example with the corresponding mutable and immutable types, we could attempt to write interfaces like so:&lt;/p&gt;
&lt;pre class="astro-code github-dark" tabindex="0" data-language="go"&gt;&lt;code&gt;&lt;span class="line"&gt;&lt;span&gt;type&lt;/span&gt;&lt;span&gt; ImmutableValue&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;T&lt;/span&gt;&lt;span&gt; ...&lt;/span&gt;&lt;span&gt;] &lt;/span&gt;&lt;span&gt;interface&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;  Mutate&lt;/span&gt;&lt;span&gt;() &lt;/span&gt;&lt;span&gt;T&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;type&lt;/span&gt;&lt;span&gt; MutableValue&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;T&lt;/span&gt;&lt;span&gt; ...&lt;/span&gt;&lt;span&gt;] &lt;/span&gt;&lt;span&gt;interface&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;  ApplyMutations&lt;/span&gt;&lt;span&gt;([]&lt;/span&gt;&lt;span&gt;Mutation&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;  Flush&lt;/span&gt;&lt;span&gt;() &lt;/span&gt;&lt;span&gt;T&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;But what do we put in place of ”…” in the example above?&lt;/p&gt;
&lt;p&gt;We can’t have the two interfaces reference each other (that is, we can’t write &lt;code&gt;type ImmutableValue[T MutableValue]&lt;/code&gt;) because &lt;code&gt;MutableValue&lt;/code&gt; isn’t a complete type. And even if we could somehow do that, prior to Go 1.26 we weren’t allowed to have both &lt;code&gt;ImmutableValue&lt;/code&gt; and &lt;code&gt;MutableValue&lt;/code&gt; reference each other in their type constraints.&lt;/p&gt;
&lt;p&gt;One option is to just use &lt;code&gt;any&lt;/code&gt;:&lt;/p&gt;
&lt;pre class="astro-code github-dark" tabindex="0" data-language="go"&gt;&lt;code&gt;&lt;span class="line"&gt;&lt;span&gt;type&lt;/span&gt;&lt;span&gt; ImmutableValue&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;T&lt;/span&gt;&lt;span&gt; any&lt;/span&gt;&lt;span&gt;] &lt;/span&gt;&lt;span&gt;interface&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;  Mutate&lt;/span&gt;&lt;span&gt;() &lt;/span&gt;&lt;span&gt;T&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;type&lt;/span&gt;&lt;span&gt; MutableValue&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;T&lt;/span&gt;&lt;span&gt; any&lt;/span&gt;&lt;span&gt;] &lt;/span&gt;&lt;span&gt;interface&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;  ApplyMutations&lt;/span&gt;&lt;span&gt;([]&lt;/span&gt;&lt;span&gt;Mutation&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;  Flush&lt;/span&gt;&lt;span&gt;() &lt;/span&gt;&lt;span&gt;T&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;But this alone isn’t enough to let us write a function like &lt;code&gt;ApplyMutations&lt;/code&gt; above: if the return type of &lt;code&gt;Mutate()&lt;/code&gt; is constrained by &lt;code&gt;any&lt;/code&gt;, then we can’t call any methods on it.&lt;/p&gt;
&lt;p&gt;As we’ll soon see, Go 1.26 added Recursive Type Parameters, which actually gives us something that we can put here. But as we’ll also see, putting a strict type constraint here isn’t actually necessary, and doesn’t actually change what the correct solution looks like.&lt;/p&gt;
&lt;h1 id="the-correct-idea-mutually-referential-type-parameters"&gt;The Correct Idea: Mutually Referential Type Parameters&lt;a class="anchor-link" aria-label="Link to heading" href="#the-correct-idea-mutually-referential-type-parameters"&gt;#&lt;/a&gt;&lt;/h1&gt;
&lt;p&gt;This is a technique originally described in &lt;a href="https://go.googlesource.com/proposal/+/refs/heads/master/design/43651-type-parameters.md#mutually-referencing-type-parameters"&gt;the original Go type parameters proposal&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The idea is to let go of the idea that every part of your data model needs to be described with interfaces. What actually matters is your functions and structs, which describe the data that they accept. An interface is powerful because it allows you to name a set of constraints and reuse them. It’s a useful tool for deduplicating constraint defitions and for composing them, but they’re a means to an end.&lt;/p&gt;
&lt;p&gt;So building on the previous example, we can use &lt;code&gt;any&lt;/code&gt; type constraints in the interface definitions, and then further constraint them when these interfaces are actually used:&lt;/p&gt;
&lt;pre class="astro-code github-dark" tabindex="0" data-language="go"&gt;&lt;code&gt;&lt;span class="line"&gt;&lt;span&gt;type&lt;/span&gt;&lt;span&gt; ImmutableValue&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;T&lt;/span&gt;&lt;span&gt; any&lt;/span&gt;&lt;span&gt;] &lt;/span&gt;&lt;span&gt;interface&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;  Mutate&lt;/span&gt;&lt;span&gt;() &lt;/span&gt;&lt;span&gt;T&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;type&lt;/span&gt;&lt;span&gt; MutableValue&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;T&lt;/span&gt;&lt;span&gt; any&lt;/span&gt;&lt;span&gt;] &lt;/span&gt;&lt;span&gt;interface&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;  ApplyMutations&lt;/span&gt;&lt;span&gt;([]&lt;/span&gt;&lt;span&gt;Mutation&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;  Flush&lt;/span&gt;&lt;span&gt;() &lt;/span&gt;&lt;span&gt;T&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;func&lt;/span&gt;&lt;span&gt; ApplyMutations&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;    ImmutableType&lt;/span&gt;&lt;span&gt; ImmutableValue&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;MutableType&lt;/span&gt;&lt;span&gt;],&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;    MutableType&lt;/span&gt;&lt;span&gt; MutableValue&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;ImmutableType&lt;/span&gt;&lt;span&gt;],&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;](&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;    immutable&lt;/span&gt;&lt;span&gt; ImmutableValue&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;    mutations&lt;/span&gt;&lt;span&gt; []&lt;/span&gt;&lt;span&gt;Mutation&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;ImmutableValue&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;  mutableValue &lt;/span&gt;&lt;span&gt;:=&lt;/span&gt;&lt;span&gt; immutable.&lt;/span&gt;&lt;span&gt;Mutate&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;  mutableValue.&lt;/span&gt;&lt;span&gt;ApplyMutations&lt;/span&gt;&lt;span&gt;(mutations)&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;  return&lt;/span&gt;&lt;span&gt; mutableValue.&lt;/span&gt;&lt;span&gt;Flush&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And for the case of the &lt;code&gt;Sequence&lt;/code&gt; type, which must be able to return itself, it can take a self-referential type constraint. These interfaces are then used in creating the full set of type constraints for a struct, which
contains our actual business logic as methods:&lt;/p&gt;
&lt;pre class="astro-code github-dark" tabindex="0" data-language="go"&gt;&lt;code&gt;&lt;span class="line"&gt;&lt;span&gt;type&lt;/span&gt;&lt;span&gt; SequenceState&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;Self&lt;/span&gt;&lt;span&gt; any&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;ValueType&lt;/span&gt;&lt;span&gt; any&lt;/span&gt;&lt;span&gt;] &lt;/span&gt;&lt;span&gt;interface&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;    CurrentValue&lt;/span&gt;&lt;span&gt;() &lt;/span&gt;&lt;span&gt;ValueType&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;    Advance&lt;/span&gt;&lt;span&gt;() (&lt;/span&gt;&lt;span&gt;nextValue&lt;/span&gt;&lt;span&gt; ValueType&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;nextState&lt;/span&gt;&lt;span&gt; Self&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;type&lt;/span&gt;&lt;span&gt; Sequence&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;Self&lt;/span&gt;&lt;span&gt; any&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;StateType&lt;/span&gt;&lt;span&gt; any&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;ValueType&lt;/span&gt;&lt;span&gt; any&lt;/span&gt;&lt;span&gt;] &lt;/span&gt;&lt;span&gt;interface&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;    CurrentState&lt;/span&gt;&lt;span&gt;() &lt;/span&gt;&lt;span&gt;StateType&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;    UpdateFromGlobalState&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;StateType&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;Self&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;type&lt;/span&gt;&lt;span&gt; SequenceTracker&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;    ValueType&lt;/span&gt;&lt;span&gt; any&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;    StateType&lt;/span&gt;&lt;span&gt; SequenceState&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;StateType&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;ValueType&lt;/span&gt;&lt;span&gt;],&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;    SequenceType&lt;/span&gt;&lt;span&gt; Sequence&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;SequenceType&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;StateType&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;ValueType&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;] &lt;/span&gt;&lt;span&gt;struct&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;    ...&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Note that even though the interface defintions don’t enforce any requirements for their type parameters, the code is still fully type-safe because the types are fully constrained where they’re actually used.&lt;/p&gt;
&lt;p&gt;The one downside is that the function and struct defintions themselves can become quite verbose. And every function that accepts these types must duplicate the type constrainted. The interface definitions can reduce the verbosity but don’t eliminate it. Fortunately, all these types can be inferred at the callsite, so the callsite remains clean.&lt;/p&gt;
&lt;p&gt;Go 1.26 allows us to further constrain the interface definitions by allowing the type constraints to reference the interface type being defined:&lt;/p&gt;
&lt;pre class="astro-code github-dark" tabindex="0" data-language="go"&gt;&lt;code&gt;&lt;span class="line"&gt;&lt;span&gt;type&lt;/span&gt;&lt;span&gt; SequenceState&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;    Self&lt;/span&gt;&lt;span&gt; SequenceState&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;Self&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;ValueType&lt;/span&gt;&lt;span&gt;],&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;    ValueType&lt;/span&gt;&lt;span&gt; any&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;] &lt;/span&gt;&lt;span&gt;interface&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;    CurrentValue&lt;/span&gt;&lt;span&gt;() &lt;/span&gt;&lt;span&gt;ValueType&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;    Advance&lt;/span&gt;&lt;span&gt;() (&lt;/span&gt;&lt;span&gt;nextValue&lt;/span&gt;&lt;span&gt; ValueType&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;nextState&lt;/span&gt;&lt;span&gt; Self&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;type&lt;/span&gt;&lt;span&gt; Sequence&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;    Self&lt;/span&gt;&lt;span&gt; Sequence&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;Self&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;StateType&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;ValueType&lt;/span&gt;&lt;span&gt;],&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;    StateType&lt;/span&gt;&lt;span&gt; SequenceState&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;Self&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;ValueType&lt;/span&gt;&lt;span&gt;],&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;    ValueType&lt;/span&gt;&lt;span&gt; any&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;] &lt;/span&gt;&lt;span&gt;interface&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;    CurrentState&lt;/span&gt;&lt;span&gt;() &lt;/span&gt;&lt;span&gt;StateType&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;    UpdateFromGlobalState&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;StateType&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;Self&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;However, this does not reduce any type constraints in the functions and structs that use these interfaces. In my experience the main benefit of this is to make the types more self-documenting, not to provide additional type safety.&lt;/p&gt;
&lt;h1 id="the-blind-spot"&gt;The Blind Spot&lt;a class="anchor-link" aria-label="Link to heading" href="#the-blind-spot"&gt;#&lt;/a&gt;&lt;/h1&gt;
&lt;p&gt;Everything example I’ve shown except for the last one has existed since Go 1.18, when type parameters were first added. So why didn’t I identify the proper solution previously?&lt;/p&gt;
&lt;p&gt;There were two facets that I think blindsided me: the use of &lt;code&gt;any&lt;/code&gt; within type constraints, and the ambiguous documentation around self-referential type constraints.&lt;/p&gt;
&lt;h2 id="any-type-constraints"&gt;&lt;code&gt;any&lt;/code&gt; Type Constraints&lt;a class="anchor-link" aria-label="Link to heading" href="#any-type-constraints"&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;It’s generally discouraged to use &lt;code&gt;any&lt;/code&gt; as a parameter type, and recommended to use the most specific type possible in interfaces and APIs. Given that advice, I had attempted to avoid using &lt;code&gt;any&lt;/code&gt; as a type constraint outside of situations where it was explicitly expected to support any type. Then, because it wasn’t possible to fully express these constraints within interface definitions prior to 1.26, I concluded that it wasn’t possible to write valid interface definitions for this situation.&lt;/p&gt;
&lt;p&gt;But in fact, it’s perfectly fine to use &lt;code&gt;any&lt;/code&gt; as a type constraint, and it doesn’t mean that the code you write will have to accept &lt;code&gt;any&lt;/code&gt; as a variable type.&lt;/p&gt;
&lt;p&gt;Think of a generic type for a data structure:&lt;/p&gt;
&lt;pre class="astro-code github-dark" tabindex="0" data-language="go"&gt;&lt;code&gt;&lt;span class="line"&gt;&lt;span&gt;type&lt;/span&gt;&lt;span&gt; MinHeap&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;T&lt;/span&gt;&lt;span&gt; any&lt;/span&gt;&lt;span&gt;] &lt;/span&gt;&lt;span&gt;interface&lt;/span&gt;&lt;span&gt; { &lt;/span&gt;&lt;span&gt;...&lt;/span&gt;&lt;span&gt; }&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This doesn’t imply that you’re actually going to specialize it with &lt;code&gt;any&lt;/code&gt;. It’s just a constraint. Don’t be afraid of &lt;code&gt;any&lt;/code&gt; in type constraints.&lt;/p&gt;
&lt;h2 id="ambiguity-in-documentation"&gt;Ambiguity in documentation&lt;a class="anchor-link" aria-label="Link to heading" href="#ambiguity-in-documentation"&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The original proposal for type parameters describes the above pattern as “mutually referencing type parameters”. And indeed, the only examples provided are cases where two type parameters reference each other. There are no examples of type parameters that reference themselves.&lt;/p&gt;
&lt;p&gt;When I initially attempting to write a type constraint for the &lt;code&gt;ImmutableValue&lt;/code&gt; interface above, I attempted to write the
version with the recursive type constraints, which would not be supported until 1.26. When this was rejected, I incorrectly assumed that it was the self-referential nature of the type constraint that made it not allowed. In fact, it has always been allowed for a type constraint to reference its own type parameter.&lt;/p&gt;
&lt;h1 id="conclusion"&gt;Conclusion&lt;a class="anchor-link" aria-label="Link to heading" href="#conclusion"&gt;#&lt;/a&gt;&lt;/h1&gt;
&lt;p&gt;Go isn’t like other languages, and its worth learning its idioms and coding patterns. I still have my gripes: I don’t like how verbose this approach is, how it contains duplicate type constraints at every generic function that needs to operate on the defined types. But adopting the recommended coding styles has also helped to expand how I think about generic code in Go.&lt;/p&gt;
&lt;p&gt;As always, if you have thoughts or if you want to tell me how wrong I am, feel free to join our &lt;a href="https://discord.gg/gqr7K4VNKe"&gt;Discord&lt;/a&gt; and shoot me a message.&lt;/p&gt;
&lt;section data-footnotes="" class="footnotes"&gt;&lt;h2 class="sr-only" id="footnote-label"&gt;Footnotes&lt;a class="anchor-link" aria-label="Link to heading" href="#footnote-label"&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;ol&gt;
&lt;li id="user-content-fn-1"&gt;
&lt;p&gt;But you can still read it &lt;a href="https://www.dolthub.com/blog/2024-11-22-are-golang-generics-simple-or-incomplete-1/"&gt;here&lt;/a&gt;, if you like. &lt;a href="#user-content-fnref-1" data-footnote-backref="" aria-label="Back to reference 1" class="data-footnote-backref"&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;</content:encoded>
      <dc:creator>Nick Tobey</dc:creator>
      <category>golang</category>
    </item>
    <item>
      <title>Introducing the Hosted Dolt REST API</title>
      <link>https://dolthub.com/blog/2026-08-20-hosted-rest-api/</link>
      <guid isPermaLink="true">https://dolthub.com/blog/2026-08-20-hosted-rest-api/</guid>
      <description>Hosted Dolt now has an official, versioned REST API for creating and managing deployments. It's built on the same OpenAPI contract we used for the DoltHub API v2, so both APIs return the same envelope, the same errors, and the same auth. Here's how to use it.</description>
      <pubDate>Thu, 20 Aug 2026 00:00:00 GMT</pubDate>
      <content:encoded>&lt;p&gt;&lt;a href="https://hosted.doltdb.com/"&gt;Hosted Dolt&lt;/a&gt; is for running online, production &lt;a href="https://github.com/dolthub/dolt"&gt;Dolt&lt;/a&gt; and &lt;a href="https://github.com/dolthub/doltgresql"&gt;Doltgres&lt;/a&gt; databases. You choose the server and disk you need, and we provision the resources and run the database for you, complete with logging, metrics, backups, and upgrades.&lt;/p&gt;
&lt;p&gt;Up until now, the only way to create or manage one of those deployments was from the web UI. Today I’m happy to announce that Hosted Dolt has an official REST API. It’s live at &lt;code&gt;https://hosted.doltdb.com/api/v1/&lt;/code&gt;, and it’s documented at &lt;a href="https://www.dolthub.com/docs/products/hosted/api/v1"&gt;dolthub.com/docs/products/hosted/api/v1&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;If you’ve used &lt;a href="https://www.dolthub.com/blog/2026-07-09-dolthub-api-v2/"&gt;the DoltHub API v2 we released last month&lt;/a&gt;, this one will look familiar because it’s purposely built on the same contract.&lt;/p&gt;
&lt;h2 id="motivation"&gt;Motivation&lt;a class="anchor-link" aria-label="Link to heading" href="#motivation"&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Hosted Dolt has had two programmatic surfaces for a while. Both are good at what they were designed for, but neither was designed specifically for managing deployments.&lt;/p&gt;
&lt;p&gt;The first is your deployment’s SQL endpoint. That’s a live Dolt or Doltgres server, so anything Dolt can do, you can do over a normal MySQL or Postgres connection, including &lt;a href="https://docs.dolthub.com/sql-reference/version-control"&gt;branches, merges, diffs, and the rest of the version control system tables and procedures&lt;/a&gt;. This is limited to the data within your database and can not be used to provision or manage the deployment itself.&lt;/p&gt;
&lt;p&gt;The second is the GraphQL API behind the Hosted website. A little over a year ago we wrote a blog called &lt;a href="https://www.dolthub.com/blog/2025-04-03-hosted-graphql-api/"&gt;“Hosted Dolt’s Hidden GraphQL API”&lt;/a&gt;, which walked through pulling the &lt;code&gt;hostedToken&lt;/code&gt; cookie out of your browser dev tools and hand-writing GraphQL queries against it. That post opened with a warning that it wasn’t an official API and could change at any time. People used it anyway because it was the only programmatic option for managing deployments.&lt;/p&gt;
&lt;p&gt;Creating and managing deployments and instances will always be available from the web UI. But we believe &lt;a href="https://www.dolthub.com/blog/2025-03-17-dolt-agentic-workflows/"&gt;Dolt is the database for agents&lt;/a&gt;, and in the age of agents it because increasingly important to provide an API an agent can use. Branching and diffing let an agent work in isolation and have changes audited before they merge, and you’ve been able to &lt;a href="https://www.dolthub.com/blog/2026-02-03-hosted-dolt-mcp/"&gt;connect an agent to a Hosted deployment over MCP&lt;/a&gt; since February. Giving it a documented REST API with real status codes and a stable error model means it can provision the database it works in too, so the whole loop, from creating a deployment to querying it to shutting it down when it’s done, is something an agent can run end to end.&lt;/p&gt;
&lt;h2 id="built-on-the-dolthub-api-v2"&gt;Built on the DoltHub API v2&lt;a class="anchor-link" aria-label="Link to heading" href="#built-on-the-dolthub-api-v2"&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;When we built &lt;a href="https://www.dolthub.com/blog/2026-07-09-dolthub-api-v2/"&gt;the DoltHub API v2&lt;/a&gt;, the goal was a contract-first API where an &lt;a href="https://spec.openapis.org/oas/v3.1.0"&gt;OpenAPI 3.1&lt;/a&gt; spec is the source of truth and everything else is generated from it: the published docs, the TypeScript types, the runtime request validation, and the contract tests. Adding an endpoint means editing the spec first, and anything that doesn’t match the spec doesn’t build.&lt;/p&gt;
&lt;p&gt;That worked well enough that we reused the whole thing for Hosted. &lt;code&gt;openapi/v1.yaml&lt;/code&gt; is the contract, the generated types are checked in and verified against the spec in CI, request bodies are validated at runtime against the spec’s schemas, and &lt;a href="https://www.oasdiff.com/"&gt;a breaking change check&lt;/a&gt; runs on every pull request. Both specs are even gated by the same CI workflow, one matrix entry each, so the two APIs can’t drift in how we detect breakage.&lt;/p&gt;
&lt;p&gt;More importantly, the parts you actually touch as a caller are the same:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;One success envelope.&lt;/strong&gt; Every 2xx body is &lt;code&gt;{ "data": ..., "meta": ... }&lt;/code&gt;. &lt;code&gt;data&lt;/code&gt; is the resource or the array of resources, and &lt;code&gt;meta&lt;/code&gt; is optional.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;One error model.&lt;/strong&gt; Every non-2xx response is an &lt;a href="https://www.rfc-editor.org/rfc/rfc9457.html"&gt;RFC 9457&lt;/a&gt; problem document, with a stable &lt;code&gt;code&lt;/code&gt; in &lt;code&gt;SCREAMING_SNAKE_CASE&lt;/code&gt; that you can branch on instead of parsing English.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cursor pagination.&lt;/strong&gt; Where a list paginates, the response has a &lt;code&gt;meta.next_page_token&lt;/code&gt; that you pass back as &lt;code&gt;page_token&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Bearer auth.&lt;/strong&gt; &lt;code&gt;Authorization: Bearer &amp;#x3C;token&gt;&lt;/code&gt;, &lt;code&gt;401&lt;/code&gt; if the credential is missing or bad, &lt;code&gt;403&lt;/code&gt; if it’s valid but not allowed.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;snake_case&lt;/code&gt; everywhere&lt;/strong&gt;, and an &lt;code&gt;x-request-id&lt;/code&gt; on every response, including successful ones.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;There’s one deliberate difference. On DoltHub, endpoints are public unless they opt in to auth, because public database reads are its baseline. On Hosted, every endpoint requires a token and has to opt out. Hosted’s control plane has nothing that’s anonymously readable, so we flipped the default.&lt;/p&gt;
&lt;h2 id="what-it-covers"&gt;What it covers&lt;a class="anchor-link" aria-label="Link to heading" href="#what-it-covers"&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;There are eleven endpoints today, and they cover your deployments, the instances behind them, their backups and configuration, and the options you can create them with.&lt;/p&gt;





















































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Endpoint&lt;/th&gt;&lt;th&gt;What it does&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;GET /api/v1/user&lt;/code&gt;&lt;/td&gt;&lt;td&gt;The authenticated user’s profile&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;GET /api/v1/deployment-options&lt;/code&gt;&lt;/td&gt;&lt;td&gt;The zones, instance types, and storage a deployment can be created with&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;POST /api/v1/deployments&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Create a deployment&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;GET /api/v1/deployments/{owner}&lt;/code&gt;&lt;/td&gt;&lt;td&gt;List an owner’s deployments&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;GET /api/v1/deployments/{owner}/{deployment}&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Get a deployment&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;POST /api/v1/deployments/{owner}/{deployment}/disable&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Disable a deployment&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;GET /api/v1/deployments/{owner}/{deployment}/instances&lt;/code&gt;&lt;/td&gt;&lt;td&gt;List the instances behind a deployment&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;POST /api/v1/deployments/{owner}/{deployment}/instances&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Add a read replica&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;DELETE /api/v1/deployments/{owner}/{deployment}/instances/{id}&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Remove an instance&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;GET /api/v1/deployments/{owner}/{deployment}/backups&lt;/code&gt;&lt;/td&gt;&lt;td&gt;List a deployment’s backups&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;GET /api/v1/deployments/{owner}/{deployment}/config&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Get a deployment’s database configuration&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;This API is the deployment control plane only. Querying the data inside a deployment isn’t part of this API and won’t be. Your deployment already exposes a SQL endpoint you connect to directly with your own database credentials, and that’s a much better interface for queries than anything we’d put over HTTP. For the same reason, your database credentials are deliberately not part of the deployment resource, so reading a deployment never hands out a credential.&lt;/p&gt;
&lt;h2 id="getting-a-token"&gt;Getting a token&lt;a class="anchor-link" aria-label="Link to heading" href="#getting-a-token"&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;First you’ll need a token. Create one from the &lt;a href="https://hosted.doltdb.com/settings/tokens"&gt;Tokens section of your user settings&lt;/a&gt;. Hosted API tokens are prefixed &lt;code&gt;hsat.v1.&lt;/code&gt;, they carry the same permissions as the user who created them, and they expire on a date you pick.&lt;/p&gt;
&lt;pre class="astro-code github-dark" tabindex="0" data-language="bash"&gt;&lt;code&gt;&lt;span class="line"&gt;&lt;span&gt;export&lt;/span&gt;&lt;span&gt; HOSTED_TOKEN&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;hsat.v1.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The quickest way to check it works is &lt;code&gt;GET /api/v1/user&lt;/code&gt;. A &lt;code&gt;200&lt;/code&gt; tells you the token is good and whose access it carries.&lt;/p&gt;
&lt;pre class="astro-code github-dark" tabindex="0" data-language="bash"&gt;&lt;code&gt;&lt;span class="line"&gt;&lt;span&gt;curl&lt;/span&gt;&lt;span&gt; -s&lt;/span&gt;&lt;span&gt; 'https://hosted.doltdb.com/api/v1/user'&lt;/span&gt;&lt;span&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;  -H&lt;/span&gt;&lt;span&gt; "Authorization: Bearer &lt;/span&gt;&lt;span&gt;$HOSTED_TOKEN&lt;/span&gt;&lt;span&gt;"&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class="astro-code github-dark" tabindex="0" data-language="json"&gt;&lt;code&gt;&lt;span class="line"&gt;&lt;span&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;  "data"&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;    "username"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;"acme-ops"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;    "display_name"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;"Acme Operations"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;    "company"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;"Acme Corp"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;    "email_addresses"&lt;/span&gt;&lt;span&gt;: [&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;      { &lt;/span&gt;&lt;span&gt;"address"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;"ops@acme.com"&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;"is_verified"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;"is_primary"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;span&gt; }&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;    ]&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id="creating-a-deployment"&gt;Creating a deployment&lt;a class="anchor-link" aria-label="Link to heading" href="#creating-a-deployment"&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;h3 id="1-see-what-you-can-create"&gt;1. See what you can create&lt;a class="anchor-link" aria-label="Link to heading" href="#1-see-what-you-can-create"&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;GET /api/v1/deployment-options&lt;/code&gt; tells you the options you can use to create a deployment or instance. It narrows in steps, since each choice depends on the one before it. Pass &lt;code&gt;cloud&lt;/code&gt; on its own to get its zones, add &lt;code&gt;zone&lt;/code&gt; to also get that zone’s instance types, and add &lt;code&gt;instance_type_id&lt;/code&gt; to also get the storage that works with that instance. Anything you haven’t narrowed enough to determine is left out rather than returned empty.&lt;/p&gt;
&lt;pre class="astro-code github-dark" tabindex="0" data-language="bash"&gt;&lt;code&gt;&lt;span class="line"&gt;&lt;span&gt;curl&lt;/span&gt;&lt;span&gt; -s&lt;/span&gt;&lt;span&gt; -G&lt;/span&gt;&lt;span&gt; 'https://hosted.doltdb.com/api/v1/deployment-options'&lt;/span&gt;&lt;span&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;  -H&lt;/span&gt;&lt;span&gt; "Authorization: Bearer &lt;/span&gt;&lt;span&gt;$HOSTED_TOKEN&lt;/span&gt;&lt;span&gt;"&lt;/span&gt;&lt;span&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;  -d&lt;/span&gt;&lt;span&gt; cloud=aws&lt;/span&gt;&lt;span&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;  -d&lt;/span&gt;&lt;span&gt; zone=us-east-1&lt;/span&gt;&lt;span&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;  -d&lt;/span&gt;&lt;span&gt; instance_type_id=aws.t2.medium&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class="astro-code github-dark" tabindex="0" data-language="json"&gt;&lt;code&gt;&lt;span class="line"&gt;&lt;span&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;  "data"&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;    "cloud"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;"aws"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;    "zones"&lt;/span&gt;&lt;span&gt;: [&lt;/span&gt;&lt;span&gt;"us-east-1"&lt;/span&gt;&lt;span&gt;],&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;    "instance_types"&lt;/span&gt;&lt;span&gt;: [&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;      {&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;        "id"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;"aws.t2.medium"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;        "name"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;"t2.medium"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;        "cpus"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;2&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;        "memory_gb"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;4&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;        "description"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;"Trial tier, the lowest spec that runs a Dolt SQL server."&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;        "hourly_cost_usd"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;0.06849315&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;      }&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;    ],&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;    "storage_options"&lt;/span&gt;&lt;span&gt;: [&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;      {&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;        "id"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;"aws.ebs.gp3_50"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;        "name"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;"Trial 50GB EBS"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;        "description"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;"Trial tier storage capped at 50GB"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;        "min_size_gb"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;50&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;        "max_size_gb"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;50&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;        "monthly_cost_usd_per_gb"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;      }&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;    ]&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Note that you will need the &lt;code&gt;id&lt;/code&gt;, not the &lt;code&gt;name&lt;/code&gt;, for the create deployment endpoint.&lt;/p&gt;
&lt;h3 id="2-create-it"&gt;2. Create it&lt;a class="anchor-link" aria-label="Link to heading" href="#2-create-it"&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;pre class="astro-code github-dark" tabindex="0" data-language="bash"&gt;&lt;code&gt;&lt;span class="line"&gt;&lt;span&gt;curl&lt;/span&gt;&lt;span&gt; -s&lt;/span&gt;&lt;span&gt; -X&lt;/span&gt;&lt;span&gt; POST&lt;/span&gt;&lt;span&gt; 'https://hosted.doltdb.com/api/v1/deployments'&lt;/span&gt;&lt;span&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;  -H&lt;/span&gt;&lt;span&gt; 'Content-Type: application/json'&lt;/span&gt;&lt;span&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;  -H&lt;/span&gt;&lt;span&gt; "Authorization: Bearer &lt;/span&gt;&lt;span&gt;$HOSTED_TOKEN&lt;/span&gt;&lt;span&gt;"&lt;/span&gt;&lt;span&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;  -d&lt;/span&gt;&lt;span&gt; '{&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;    "owner": "acme",&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;    "name": "analytics",&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;    "cloud": "aws",&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;    "zone": "us-east-1",&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;    "instance_type_id": "aws.t2.medium",&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;    "volume_type_id": "aws.ebs.gp3_50",&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;    "volume_size_gb": 50&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;  }'&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You get back a &lt;code&gt;202&lt;/code&gt;, meaning we’ve accepted the request and provisioning continues after the response. &lt;code&gt;host&lt;/code&gt; is empty until the deployment comes up.&lt;/p&gt;
&lt;pre class="astro-code github-dark" tabindex="0" data-language="json"&gt;&lt;code&gt;&lt;span class="line"&gt;&lt;span&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;  "data"&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;    "owner"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;"acme"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;    "name"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;"analytics"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;    "state"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;"starting"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;    "cloud"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;"aws"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;    "zone"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;"us-east-1"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;    "cluster_type"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;"dolt"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;    "instance_type_name"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;"t2.medium"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;    "volume_type_name"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;"Trial 50GB EBS"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;    "volume_size_gb"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;50&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;    "replicas"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;    "host"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;""&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;    "port"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;3306&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;    "caller_role"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;"admin"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;    "created_by"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;"acme-ops"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;    "created_at"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;"2026-08-11T09:14:00Z"&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;cluster_type&lt;/code&gt; defaults to &lt;code&gt;dolt&lt;/code&gt;. Pass &lt;code&gt;doltgres&lt;/code&gt; if you want a &lt;a href="https://www.dolthub.com/blog/2026-08-06-doltgres-1-0/"&gt;Doltgres&lt;/a&gt; deployment, or &lt;code&gt;mysql_with_dolt_replicas&lt;/code&gt; for a MySQL primary with Dolt read replicas.&lt;/p&gt;
&lt;h3 id="3-wait-for-it-to-start"&gt;3. Wait for it to start&lt;a class="anchor-link" aria-label="Link to heading" href="#3-wait-for-it-to-start"&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;The DoltHub API v2 routes its async work through a single &lt;code&gt;Operation&lt;/code&gt; resource that you poll. We didn’t need one here, because the deployment is already the thing whose state you care about. So you poll the deployment until &lt;code&gt;state&lt;/code&gt; is &lt;code&gt;started&lt;/code&gt;.&lt;/p&gt;
&lt;pre class="astro-code github-dark" tabindex="0" data-language="bash"&gt;&lt;code&gt;&lt;span class="line"&gt;&lt;span&gt;curl&lt;/span&gt;&lt;span&gt; -s&lt;/span&gt;&lt;span&gt; 'https://hosted.doltdb.com/api/v1/deployments/acme/analytics'&lt;/span&gt;&lt;span&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;  -H&lt;/span&gt;&lt;span&gt; "Authorization: Bearer &lt;/span&gt;&lt;span&gt;$HOSTED_TOKEN&lt;/span&gt;&lt;span&gt;"&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class="astro-code github-dark" tabindex="0" data-language="json"&gt;&lt;code&gt;&lt;span class="line"&gt;&lt;span&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;  "data"&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;    "owner"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;"acme"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;    "name"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;"analytics"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;    "state"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;"started"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;    "cloud"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;"aws"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;    "zone"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;"us-east-1"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;    "cluster_type"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;"dolt"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;    "instance_type_name"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;"t2.medium"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;    "volume_type_name"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;"Trial 50GB EBS"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;    "volume_size_gb"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;50&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;    "replicas"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;    "database_version"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;"1.58.4"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;    "host"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;"analytics.dbs.hosted.doltdb.com"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;    "port"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;3306&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;    "hourly_cost_usd"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;0.06849315&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;    "webpki_cert"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;    "expose_remotesapi_endpoint"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;false&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;    "expose_mcp"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;false&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;    "expose_stats"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;false&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;    "disable_automatic_dolt_updates"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;false&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;    "caller_role"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;"admin"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;    "created_by"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;"acme-ops"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;    "created_at"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;"2026-07-01T18:22:04Z"&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;host&lt;/code&gt; and &lt;code&gt;port&lt;/code&gt; are your connection details, and from here you’re in normal Dolt territory.&lt;/p&gt;
&lt;pre class="astro-code github-dark" tabindex="0" data-language="bash"&gt;&lt;code&gt;&lt;span class="line"&gt;&lt;span&gt;mysql&lt;/span&gt;&lt;span&gt; -h&lt;/span&gt;&lt;span&gt; analytics.dbs.hosted.doltdb.com&lt;/span&gt;&lt;span&gt; -P&lt;/span&gt;&lt;span&gt; 3306&lt;/span&gt;&lt;span&gt; -u&lt;/span&gt;&lt;span&gt; &amp;#x3C;&lt;/span&gt;&lt;span&gt;use&lt;/span&gt;&lt;span&gt;r&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;span&gt; -p&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The same &lt;code&gt;starting&lt;/code&gt; to &lt;code&gt;started&lt;/code&gt; transition covers restarts and resizes too, so the same poll works for those.&lt;/p&gt;
&lt;h2 id="inspecting-a-deployment"&gt;Inspecting a deployment&lt;a class="anchor-link" aria-label="Link to heading" href="#inspecting-a-deployment"&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;GET /api/v1/deployments/{owner}&lt;/code&gt; lists deployments for an owner name. It takes an optional &lt;code&gt;state&lt;/code&gt; filter and it paginates. The items are a summary rather than the full deployment, so it drops the connection details and adds last backup information.&lt;/p&gt;
&lt;pre class="astro-code github-dark" tabindex="0" data-language="bash"&gt;&lt;code&gt;&lt;span class="line"&gt;&lt;span&gt;curl&lt;/span&gt;&lt;span&gt; -s&lt;/span&gt;&lt;span&gt; -G&lt;/span&gt;&lt;span&gt; 'https://hosted.doltdb.com/api/v1/deployments/acme'&lt;/span&gt;&lt;span&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;  -H&lt;/span&gt;&lt;span&gt; "Authorization: Bearer &lt;/span&gt;&lt;span&gt;$HOSTED_TOKEN&lt;/span&gt;&lt;span&gt;"&lt;/span&gt;&lt;span&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;  -d&lt;/span&gt;&lt;span&gt; state=started&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class="astro-code github-dark" tabindex="0" data-language="json"&gt;&lt;code&gt;&lt;span class="line"&gt;&lt;span&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;  "data"&lt;/span&gt;&lt;span&gt;: [&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;    {&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;      "owner"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;"acme"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;      "name"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;"analytics"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;      "state"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;"started"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;      "cloud"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;"aws"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;      "zone"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;"us-west-2"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;      "cluster_type"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;"dolt"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;      "instance_type_name"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;"m5.large"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;      "volume_type_name"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;"gp3"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;      "volume_size_gb"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;100&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;      "replicas"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;      "database_version"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;"1.58.4"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;      "hourly_cost_usd"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;0.192&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;      "webpki_cert"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;      "last_backup_time"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;"2026-08-10T02:00:00Z"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;      "last_backup_size_bytes"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;1048576&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;  ],&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;  "meta"&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;    "next_page_token"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;"eyJvZmZzZXQiOjI1fQ"&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Pass the &lt;code&gt;next_page_token&lt;/code&gt; back as &lt;code&gt;page_token&lt;/code&gt; for the next page, and stop when &lt;code&gt;meta&lt;/code&gt; isn’t there anymore.&lt;/p&gt;
&lt;p&gt;You can list the backups we’re holding for a deployment, newest first:&lt;/p&gt;
&lt;pre class="astro-code github-dark" tabindex="0" data-language="bash"&gt;&lt;code&gt;&lt;span class="line"&gt;&lt;span&gt;curl&lt;/span&gt;&lt;span&gt; -s&lt;/span&gt;&lt;span&gt; 'https://hosted.doltdb.com/api/v1/deployments/acme/analytics/backups'&lt;/span&gt;&lt;span&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;  -H&lt;/span&gt;&lt;span&gt; "Authorization: Bearer &lt;/span&gt;&lt;span&gt;$HOSTED_TOKEN&lt;/span&gt;&lt;span&gt;"&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class="astro-code github-dark" tabindex="0" data-language="json"&gt;&lt;code&gt;&lt;span class="line"&gt;&lt;span&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;  "data"&lt;/span&gt;&lt;span&gt;: [&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;    {&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;      "id"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;"20260812T020000.000"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;      "databases"&lt;/span&gt;&lt;span&gt;: [&lt;/span&gt;&lt;span&gt;"analytics"&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;"staging"&lt;/span&gt;&lt;span&gt;],&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;      "instance_index"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;      "created_at"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;"2026-08-12T02:00:00Z"&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;    },&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;    {&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;      "id"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;"20260811T020000.000"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;      "databases"&lt;/span&gt;&lt;span&gt;: [&lt;/span&gt;&lt;span&gt;"analytics"&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;"staging"&lt;/span&gt;&lt;span&gt;],&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;      "size_bytes"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;1048576&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;      "instance_index"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;      "created_at"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;"2026-08-11T02:00:00Z"&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;  ]&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The newest backup there doesn’t have a &lt;code&gt;size_bytes&lt;/code&gt; yet. That’s expected, since we measure it asynchronously after the backup is taken, so a recent one has no size for a few minutes.&lt;/p&gt;
&lt;p&gt;And you can read a deployment’s database configuration. This returns every setting Hosted supports, at the value the deployment is actually running, which is the same thing the Configuration page in the UI shows you. &lt;code&gt;is_overridden&lt;/code&gt; tells you whether you changed it, and &lt;code&gt;default&lt;/code&gt; tells you what it would go back to.&lt;/p&gt;
&lt;pre class="astro-code github-dark" tabindex="0" data-language="bash"&gt;&lt;code&gt;&lt;span class="line"&gt;&lt;span&gt;curl&lt;/span&gt;&lt;span&gt; -s&lt;/span&gt;&lt;span&gt; 'https://hosted.doltdb.com/api/v1/deployments/acme/analytics/config'&lt;/span&gt;&lt;span&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;  -H&lt;/span&gt;&lt;span&gt; "Authorization: Bearer &lt;/span&gt;&lt;span&gt;$HOSTED_TOKEN&lt;/span&gt;&lt;span&gt;"&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class="astro-code github-dark" tabindex="0" data-language="json"&gt;&lt;code&gt;&lt;span class="line"&gt;&lt;span&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;  "data"&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;    "settings"&lt;/span&gt;&lt;span&gt;: [&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;      {&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;        "key"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;"listener_max_connections"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;        "value"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;"500"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;        "default"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;"100"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;        "is_overridden"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;      },&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;      {&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;        "key"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;"behavior_read_only"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;        "value"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;"false"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;        "default"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;"false"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;        "is_overridden"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;false&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;      }&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;    ]&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Values come back as strings exactly as they’re stored, including the numeric and boolean ones.&lt;/p&gt;
&lt;h2 id="disabling-a-deployment"&gt;Disabling a deployment&lt;a class="anchor-link" aria-label="Link to heading" href="#disabling-a-deployment"&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;POST .../disable&lt;/code&gt; tears a deployment’s instances and storage down. You get a &lt;code&gt;202&lt;/code&gt; with the deployment in &lt;code&gt;stopping&lt;/code&gt;, and you poll the deployment until it’s &lt;code&gt;stopped&lt;/code&gt;.&lt;/p&gt;
&lt;pre class="astro-code github-dark" tabindex="0" data-language="bash"&gt;&lt;code&gt;&lt;span class="line"&gt;&lt;span&gt;curl&lt;/span&gt;&lt;span&gt; -s&lt;/span&gt;&lt;span&gt; -X&lt;/span&gt;&lt;span&gt; POST&lt;/span&gt;&lt;span&gt; 'https://hosted.doltdb.com/api/v1/deployments/acme/analytics/disable'&lt;/span&gt;&lt;span&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;  -H&lt;/span&gt;&lt;span&gt; "Authorization: Bearer &lt;/span&gt;&lt;span&gt;$HOSTED_TOKEN&lt;/span&gt;&lt;span&gt;"&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class="astro-code github-dark" tabindex="0" data-language="json"&gt;&lt;code&gt;&lt;span class="line"&gt;&lt;span&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;  "data"&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;    "owner"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;"acme"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;    "name"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;"analytics"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;    "state"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;"stopping"&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Take a backup first if you want the data.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The deployment record itself sticks around. It stays readable with &lt;code&gt;disabled_at&lt;/code&gt; and &lt;code&gt;disabled_by&lt;/code&gt; set, which is why this is a &lt;code&gt;POST&lt;/code&gt; to an action rather than a &lt;code&gt;DELETE&lt;/code&gt; on the deployment. To bring it back, add an instance to it, which clears the shutdown and starts it up again. Pass a &lt;code&gt;backup_id&lt;/code&gt; from the backups list on that request to restore your data into it, or it’ll come back empty.&lt;/p&gt;
&lt;h2 id="documentation"&gt;Documentation&lt;a class="anchor-link" aria-label="Link to heading" href="#documentation"&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The full reference lives at &lt;a href="https://www.dolthub.com/docs/products/hosted/api/v1"&gt;dolthub.com/docs/products/hosted/api/v1&lt;/a&gt;. Every endpoint, schema, error code, and security scheme there is rendered straight from the OpenAPI spec, so there’s no drift between what the docs say and what the server does.&lt;/p&gt;
&lt;p&gt;If you’d rather generate a typed client in your language of choice, the spec itself is in &lt;a href="https://github.com/dolthub/docs-2/blob/dev/specs/hosted-v1.yaml"&gt;our docs repo&lt;/a&gt;. Grab it and point your generator at it.&lt;/p&gt;
&lt;h2 id="future-work"&gt;Future work&lt;a class="anchor-link" aria-label="Link to heading" href="#future-work"&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;v1 is additive. We can add endpoints, optional request fields, response fields, and new error codes within v1, and we’d only need a v2 to rename or remove a field or change what an existing one means. So you can build against what’s live today without worrying that the rest of it will move underneath you.&lt;/p&gt;
&lt;p&gt;There’s more coming:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Pull requests&lt;/strong&gt;: creating, viewing, and merging them. Every other version control operation is available over SQL on your deployment, but pull request metadata lives in Hosted’s application database rather than in your Dolt database, so there’s no query that opens or manages one.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Credentials&lt;/strong&gt;: issuing and rotating a deployment’s database credentials.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Configuration writes&lt;/strong&gt;: updating deployment settings and Dolt configuration.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Deployment actions&lt;/strong&gt;: upgrading Dolt/Doltgres, rebooting an instance, and restarting an application.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Access management&lt;/strong&gt;: adding and removing collaborators and their roles.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CLIs&lt;/strong&gt;: we’re building a command line tool on top of this API and &lt;a href="https://www.dolthub.com/blog/2026-07-09-dolthub-api-v2/"&gt;the DoltHub API v2&lt;/a&gt;, along the lines of GitHub’s &lt;a href="https://cli.github.com/"&gt;&lt;code&gt;gh&lt;/code&gt;&lt;/a&gt;, so you can drive either product from your terminal without writing the HTTP calls yourself.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="conclusion"&gt;Conclusion&lt;a class="anchor-link" aria-label="Link to heading" href="#conclusion"&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Between this and &lt;a href="https://www.dolthub.com/blog/2026-07-09-dolthub-api-v2/"&gt;the DoltHub API v2&lt;/a&gt;, building against either of our products should feel like working with the same API. If it doesn’t somewhere, that’s a bug and we want to hear about it.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://hosted.doltdb.com/settings/tokens"&gt;Create a token&lt;/a&gt; and try it out. &lt;a href="https://github.com/dolthub/hosted-issues/issues"&gt;File an issue&lt;/a&gt; if there’s an endpoint you want sooner or is not covered above, or come to talk to us on &lt;a href="https://discord.gg/gqr7K4VNKe"&gt;Discord&lt;/a&gt;.&lt;/p&gt;</content:encoded>
      <dc:creator>Taylor Bantle</dc:creator>
      <category>hosted</category>
      <category>feature release</category>
    </item>
    <item>
      <title>What's New With Golang Generics</title>
      <link>https://dolthub.com/blog/2026-08-14-whats-new-with-golang-generics/</link>
      <guid isPermaLink="true">https://dolthub.com/blog/2026-08-14-whats-new-with-golang-generics/</guid>
      <description>Go is conservative with adding new features, but it does happen. Generics were new once. Here's some ways that generics have evolved in recent versions, and how Dolt incorporates them.</description>
      <pubDate>Fri, 14 Aug 2026 00:00:00 GMT</pubDate>
      <content:encoded>&lt;p&gt;I write &lt;a href="https://www.dolthub.com/"&gt;Dolt, a version controlled database written 100% in Go&lt;/a&gt;. This wasn’t a choice we made so much as a choice that was made for us, since it’s based on &lt;a href="https://github.com/attic-labs/noms"&gt;noms&lt;/a&gt; and built on &lt;a href="https://github.com/dolthub/go-mysql-server/"&gt;go-mysql-server&lt;/a&gt;, which were both written in Go.&lt;/p&gt;
&lt;p&gt;As a result, I use Golang every day, but I’m neither a devoted advocate nor a hater. Go is a tool, and given that we used it to build a drop-in replacement for MySQL &lt;a href="https://www.dolthub.com/blog/2026-01-06-more-read-performance-wins/"&gt;that’s faster than MySQL&lt;/a&gt;, it does a perfectly decent job.&lt;/p&gt;
&lt;p&gt;Go’s design philosophy is that it’s simple by design, and the language is very conservative about adding new features unless those features can be demonstrated to be actually necessary and do more good than harm. It’s a high bar, but a bar than can and has been met in the past. Remember that Go used to not support generics, but support was finally added in 1.18, after it was clear that they were needed.&lt;/p&gt;
&lt;p&gt;Since then, Go’s generic support has slowly improved. Features are added when they allow for cleaner and more expressive code, but avoided when it would only lead to messier, harder-to-maintain code. &lt;a href="https://www.dolthub.com/blog/2024-12-05-whats-missing-from-golang-generics/"&gt;Last year, I ruminated about potential features that Golang might add to generics, debating for each one whether or not it met the bar.&lt;/a&gt; I essentially asked for each feature: “Generic code was already hard to read, does adding this allow for even more complicated code? Or do they allow for cleaner ways to express existing ideas?&lt;/p&gt;
&lt;p&gt;Cut to two years later, and some of the features I ruminated about have since been more-or-less added to the language. I decided to see how these additions had impacted Dolt, whether we had adopted them and whether they had enabled us to write cleaner code.&lt;/p&gt;
&lt;h2 id="generic-type-aliases-added-in-go-124"&gt;Generic Type Aliases (Added in Go 1.24)&lt;a class="anchor-link" aria-label="Link to heading" href="#generic-type-aliases-added-in-go-124"&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Prior to Go 1.24, the following was invalid:&lt;/p&gt;
&lt;pre class="astro-code github-dark" tabindex="0" data-language="go"&gt;&lt;code&gt;&lt;span class="line"&gt;&lt;span&gt;type&lt;/span&gt;&lt;span&gt; Set&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;T&lt;/span&gt;&lt;span&gt; comparable&lt;/span&gt;&lt;span&gt;] &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; map&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;T&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;struct&lt;/span&gt;&lt;span&gt;{}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;While generic type aliases don’t add any expressiveness to the language, they can remove a lot of repetition and clutter. They’re mostly useful when you intend to repeatedly use a specific type for &lt;em&gt;some&lt;/em&gt; of a generic’s type parameters, so you partially specialize it once.&lt;/p&gt;
&lt;p&gt;I was an advocate for generic type aliases last time, and I’m excited to see that they’re supported now. But they’re also very situational: I searched Dolt’s codebase and found 0 uses. So their use cases are indeed pretty narrow.&lt;/p&gt;
&lt;p&gt;That said, now that they’re available, it’s possible they might find a home in Dolt in the future.&lt;/p&gt;
&lt;h2 id="recursive-type-constraints-added-in-go-126"&gt;Recursive Type Constraints (Added in Go 1.26)&lt;a class="anchor-link" aria-label="Link to heading" href="#recursive-type-constraints-added-in-go-126"&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;This was a feature added in Go 1.26, allowing type constraints like the following:&lt;/p&gt;
&lt;pre class="astro-code github-dark" tabindex="0" data-language="go"&gt;&lt;code&gt;&lt;span class="line"&gt;&lt;span&gt;// A Lattice is a type of weakly ordered values with a least value named Bottom, and a unique&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;// least upper bound for any two value.&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;type&lt;/span&gt;&lt;span&gt; Lattice&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;T&lt;/span&gt;&lt;span&gt; Lattice&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;T&lt;/span&gt;&lt;span&gt;]] &lt;/span&gt;&lt;span&gt;interface&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;  Less&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;other&lt;/span&gt;&lt;span&gt; T&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;bool&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;  LeastUpperBound&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;other&lt;/span&gt;&lt;span&gt; T&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;T&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;  Bottom&lt;/span&gt;&lt;span&gt;() &lt;/span&gt;&lt;span&gt;T&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;func&lt;/span&gt;&lt;span&gt; WeakTopologicalOrder&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;T&lt;/span&gt;&lt;span&gt; Lattice&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;T&lt;/span&gt;&lt;span&gt;]](&lt;/span&gt;&lt;span&gt;elems&lt;/span&gt;&lt;span&gt; []&lt;/span&gt;&lt;span&gt;T&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;seq&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Iter&lt;/span&gt;&lt;span&gt; { &lt;/span&gt;&lt;span&gt;...&lt;/span&gt;&lt;span&gt; }&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;var&lt;/span&gt;&lt;span&gt; _ &lt;/span&gt;&lt;span&gt;Lattice&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;LatticeImpl&lt;/span&gt;&lt;span&gt;] &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; LatticeImpl&lt;/span&gt;&lt;span&gt;{}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This lets us use method chaining in functions that operate on this type:&lt;/p&gt;
&lt;pre class="astro-code github-dark" tabindex="0" data-language="go"&gt;&lt;code&gt;&lt;span class="line"&gt;&lt;span&gt;func&lt;/span&gt;&lt;span&gt; NAryLeastUpperBound&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;T&lt;/span&gt;&lt;span&gt; Lattice&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;T&lt;/span&gt;&lt;span&gt;]](&lt;/span&gt;&lt;span&gt;elems&lt;/span&gt;&lt;span&gt; T&lt;/span&gt;&lt;span&gt;[]) &lt;/span&gt;&lt;span&gt;T&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;  var&lt;/span&gt;&lt;span&gt; result &lt;/span&gt;&lt;span&gt;T&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;  result &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; result.&lt;/span&gt;&lt;span&gt;Bottom&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;  for&lt;/span&gt;&lt;span&gt; _, l &lt;/span&gt;&lt;span&gt;:=&lt;/span&gt;&lt;span&gt; range&lt;/span&gt;&lt;span&gt; elems {&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;    result &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; result.&lt;/span&gt;&lt;span&gt;LeastUpperBound&lt;/span&gt;&lt;span&gt;(l)&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;  return&lt;/span&gt;&lt;span&gt; result&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In other languages, using a type parameter in its own type constraint is called a &lt;a href="https://en.wikipedia.org/wiki/Curiously_recurring_template_pattern"&gt;curiously recurring template pattern&lt;/a&gt;. It has some additional uses in languages that allow template metaprogramming, but the main value in Golang is to define an interface for types with methods that accept or return the same time being implemented.&lt;/p&gt;
&lt;p&gt;This strongly resembles the “self-type constraint” feature from my previous article, with the caveat that it’s technically possible to provide a &lt;em&gt;different&lt;/em&gt; type for the type parameter from the type that’s implementing the interface. For instance, You could have &lt;code&gt;LatticeImpl&lt;/code&gt; implement &lt;code&gt;Lattice[SomeUnrelatedLatticeImpl]&lt;/code&gt;, although you probably shouldn’t.&lt;/p&gt;
&lt;p&gt;This looks really cool… but it’s not actually accomplishing as much as you might think. Prior to Go 1.26, the above interface definition wouldn’t be allowed, but we could write one that looks like this instead:&lt;/p&gt;
&lt;pre class="astro-code github-dark" tabindex="0" data-language="go"&gt;&lt;code&gt;&lt;span class="line"&gt;&lt;span&gt;type&lt;/span&gt;&lt;span&gt; Lattice&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;T&lt;/span&gt;&lt;span&gt; any&lt;/span&gt;&lt;span&gt;] &lt;/span&gt;&lt;span&gt;interface&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;  Less&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;other&lt;/span&gt;&lt;span&gt; T&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;bool&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;  LeastUpperBound&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;other&lt;/span&gt;&lt;span&gt; T&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;T&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;  Bottom&lt;/span&gt;&lt;span&gt;() &lt;/span&gt;&lt;span&gt;T&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And our generic function above would still be just as correct. The main benefit of the recursive type constraint is that it more clearly documents intent. It’s more obvious at a glance that the generic interface is meant to be specialized with the same type that’s implementing it, and it rejects attempts to specialize it with a type that’s completely unrelated.&lt;/p&gt;
&lt;p&gt;There’s no code in Dolt that uses this feature, but there’s some code that could be updated to use it, such as the &lt;code&gt;SequencedRelation&lt;/code&gt; generic interface. This in an interface type that represents database objects that produce an incrementing sequence of values, such as MySQL’s &lt;code&gt;AUTO_INCREMENT&lt;/code&gt; columns. Implementations of this interface have a method that take a state for sequence’s state machine and return a new database object whose state is set to the provided value. Since this is a type that can return itself, it has a self-referential type parameter named Self:&lt;/p&gt;
&lt;pre class="astro-code github-dark" tabindex="0" data-language="go"&gt;&lt;code&gt;&lt;span class="line"&gt;&lt;span&gt;// an abridged version of the SequencedRelation generic interface&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;type&lt;/span&gt;&lt;span&gt; SequencedRelation&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;Self&lt;/span&gt;&lt;span&gt; any&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;StateType&lt;/span&gt;&lt;span&gt; any&lt;/span&gt;&lt;span&gt;] &lt;/span&gt;&lt;span&gt;interface&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;	// GetSequenceState returns the current SequenceState of the object.&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;	GetSequenceState&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;ctx&lt;/span&gt;&lt;span&gt; context&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Context&lt;/span&gt;&lt;span&gt;) (&lt;/span&gt;&lt;span&gt;StateType&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;error&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;	// HasSequenceState returns whether the relation wraps a sequence.&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;	// (This may be false, for instance, for tables that do not have an AUTO INCREMENT column)&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;	HasSequenceState&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;ctx&lt;/span&gt;&lt;span&gt; context&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Context&lt;/span&gt;&lt;span&gt;) (&lt;/span&gt;&lt;span&gt;bool&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;error&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;	// SetSequenceState unconditionally sets the SequenceState for the object.&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;	SetSequenceState&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;ctx&lt;/span&gt;&lt;span&gt; context&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Context&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;val&lt;/span&gt;&lt;span&gt; StateType&lt;/span&gt;&lt;span&gt;) (&lt;/span&gt;&lt;span&gt;Self&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;error&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Note how the type of this constraint is &lt;code&gt;any&lt;/code&gt;. With Go 1.26, we could use a more specific type for this constraint:&lt;/p&gt;
&lt;pre class="astro-code github-dark" tabindex="0" data-language="go"&gt;&lt;code&gt;&lt;span class="line"&gt;&lt;span&gt;// This version is more self-documenting:&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;// It's more clear what the type parameter is for and harder to misuse.&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;type&lt;/span&gt;&lt;span&gt; SequencedRelation&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;Self&lt;/span&gt;&lt;span&gt; SequencedRelation&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;Self&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;StateType&lt;/span&gt;&lt;span&gt;], &lt;/span&gt;&lt;span&gt;StateType&lt;/span&gt;&lt;span&gt; any&lt;/span&gt;&lt;span&gt;] &lt;/span&gt;&lt;span&gt;interface&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;	// GetSequenceState returns the current SequenceState of the object.&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;	GetSequenceState&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;ctx&lt;/span&gt;&lt;span&gt; context&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Context&lt;/span&gt;&lt;span&gt;) (&lt;/span&gt;&lt;span&gt;StateType&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;error&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;	// HasSequenceState returns whether the relation wraps a sequence.&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;	// (This may be false, for instance, for tables that do not have an AUTO INCREMENT column)&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;	HasSequenceState&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;ctx&lt;/span&gt;&lt;span&gt; context&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Context&lt;/span&gt;&lt;span&gt;) (&lt;/span&gt;&lt;span&gt;bool&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;error&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;	// SetSequenceState unconditionally sets the SequenceState for the object.&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;	SetSequenceState&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;ctx&lt;/span&gt;&lt;span&gt; context&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Context&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;val&lt;/span&gt;&lt;span&gt; StateType&lt;/span&gt;&lt;span&gt;) (&lt;/span&gt;&lt;span&gt;Self&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;error&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id="generic-methods-added-in-go-127"&gt;Generic Methods (Added in Go 1.27)&lt;a class="anchor-link" aria-label="Link to heading" href="#generic-methods-added-in-go-127"&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Prior to Go 1.27, while function definitions could take generic type parameters, methods could not. This was because Go is a structurally typed language: the set of methods implemented by a type determines what interfaces it implements. If a type defines generic methods, it becomes difficult for the compiler to reason about which interfaces it implements, and even more for the compiler to determine which of the infinite number of possible specializations of the method will be needed at runtime.&lt;/p&gt;
&lt;p&gt;This meant that it wasn’t possible to write generic methods, even if the method wasn’t intended to be part of an interface. For instance, if you have a function that’s tightly coupled with a type, you might want to make that function a method to have it exist in the function’s namespace. But if that function was generic, you couldn’t do that.&lt;/p&gt;
&lt;pre class="astro-code github-dark" tabindex="0" data-language="go"&gt;&lt;code&gt;&lt;span class="line"&gt;&lt;span&gt;type&lt;/span&gt;&lt;span&gt; Tree&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;T&lt;/span&gt;&lt;span&gt; any&lt;/span&gt;&lt;span&gt;] &lt;/span&gt;&lt;span&gt;struct&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;  node &lt;/span&gt;&lt;span&gt;T&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;  children []&lt;/span&gt;&lt;span&gt;Tree&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;T&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;// Prior to Go 1.27, this was not allowed&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;func&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;t &lt;/span&gt;&lt;span&gt;Tree&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;T&lt;/span&gt;&lt;span&gt;]) &lt;/span&gt;&lt;span&gt;Map&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;U&lt;/span&gt;&lt;span&gt; any&lt;/span&gt;&lt;span&gt;](&lt;/span&gt;&lt;span&gt;f&lt;/span&gt;&lt;span&gt; func&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;T&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;U&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;  result &lt;/span&gt;&lt;span&gt;:=&lt;/span&gt;&lt;span&gt; Tree[U] { node: &lt;/span&gt;&lt;span&gt;f&lt;/span&gt;&lt;span&gt;(t.node)}&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;  for&lt;/span&gt;&lt;span&gt; _, child &lt;/span&gt;&lt;span&gt;:=&lt;/span&gt;&lt;span&gt; range&lt;/span&gt;&lt;span&gt; children {&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;    result.children &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; append&lt;/span&gt;&lt;span&gt;(result.children, &lt;/span&gt;&lt;span&gt;f&lt;/span&gt;&lt;span&gt;(child))&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Instead, you would have needed to write the almost-identical function below:&lt;/p&gt;
&lt;pre class="astro-code github-dark" tabindex="0" data-language="go"&gt;&lt;code&gt;&lt;span class="line"&gt;&lt;span&gt;func&lt;/span&gt;&lt;span&gt; MapTree&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;T&lt;/span&gt;&lt;span&gt; any&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;U&lt;/span&gt;&lt;span&gt; any&lt;/span&gt;&lt;span&gt;](&lt;/span&gt;&lt;span&gt;input&lt;/span&gt;&lt;span&gt; Tree&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;T&lt;/span&gt;&lt;span&gt;]) &lt;/span&gt;&lt;span&gt;Tree&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;U&lt;/span&gt;&lt;span&gt;] {&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;  result &lt;/span&gt;&lt;span&gt;:=&lt;/span&gt;&lt;span&gt; Tree[U] { node: &lt;/span&gt;&lt;span&gt;f&lt;/span&gt;&lt;span&gt;(t.node)}&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;  for&lt;/span&gt;&lt;span&gt; _, child &lt;/span&gt;&lt;span&gt;:=&lt;/span&gt;&lt;span&gt; range&lt;/span&gt;&lt;span&gt; children {&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;    result.children &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; append&lt;/span&gt;&lt;span&gt;(result.children, &lt;/span&gt;&lt;span&gt;f&lt;/span&gt;&lt;span&gt;(child))&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With Go 1.27, types will finally be able to implement generic methods, with the condition that these methods do not participate in satisfying interface constraints. These methods won’t change the set of interfaces that the type implements but can still be used to add functions to the namespace of the type.&lt;/p&gt;
&lt;p&gt;We have functions like the above in Dolt. But none of them have been migrated to use generic methods yet, because Go 1.27 isn’t out yet. But when it releases later this month, I’ll expect we’ll find ourselves writing generic methods when it is natural to do so.&lt;/p&gt;
&lt;h1 id="overall-impact"&gt;Overall Impact&lt;a class="anchor-link" aria-label="Link to heading" href="#overall-impact"&gt;#&lt;/a&gt;&lt;/h1&gt;
&lt;p&gt;Currently, Dolt doesn’t use any of the above features, although that likely won’t be true for long. I’ve already identified places where we could incorporate recursive type constraints for a little bit of extra type strictness, and we’ll definitely be writing generic methods once we’re able to. It’s possible that there are places that would benefit from generic type aliases, and we just haven’t identified them or done the necessary refactors yet.&lt;/p&gt;
&lt;p&gt;So while none of these features have made an impact on Dolt, although I’m still glad they all exist.&lt;/p&gt;
&lt;p&gt;Next time we’ll dive more into how Dolt expresses relationships between generic types in a clean, readable way. In the meantime, I know you all have strong opinions about Golang. Feel free to join &lt;a href="https://discord.gg/gqr7K4VNKe"&gt;our Discord&lt;/a&gt; and tell me all the ways I’m wrong about the language.&lt;/p&gt;</content:encoded>
      <dc:creator>Nick Tobey</dc:creator>
      <category>golang</category>
    </item>
    <item>
      <title>Doltgres is as Fast as MySQL is Slow</title>
      <link>https://dolthub.com/blog/2026-08-13-doltgres-as-fast-as-mysql-is-slow/</link>
      <guid isPermaLink="true">https://dolthub.com/blog/2026-08-13-doltgres-as-fast-as-mysql-is-slow/</guid>
      <description>Overview of recent performance optimizations to Doltgres that brought it on par with MySQL</description>
      <pubDate>Thu, 13 Aug 2026 00:00:00 GMT</pubDate>
      <content:encoded>&lt;p&gt;It’s official! Doltgres v1.0 is &lt;a href="https://www.dolthub.com/blog/2026-08-06-doltgres-1-0/"&gt;here&lt;/a&gt;!
Alongside various features and correctness improvements, this release comes with some large performance gains.
Over the course of a few months, we’ve managed to reduce Doltgres from &lt;code&gt;4.4x&lt;/code&gt; to &lt;code&gt;2.6x&lt;/code&gt; Postgres on sysbench, which is a &lt;code&gt;1.8x&lt;/code&gt; improvement!
Fun fact, MySQL’s average multiplier when compared against Postgres on these same benchmarks is also &lt;code&gt;2.6x&lt;/code&gt;.
This means that Doltgres is as fast as MySQL is slow.&lt;/p&gt;
&lt;h1 id="overview"&gt;Overview&lt;a class="anchor-link" aria-label="Link to heading" href="#overview"&gt;#&lt;/a&gt;&lt;/h1&gt;
&lt;p&gt;There were a wide variety of optimizations over various parts of the codebase, including improvements to wire format serialization, collections, query analysis, and index costing.
In summary, here are the latency numbers from Doltgres &lt;code&gt;v0.56.3&lt;/code&gt; to &lt;code&gt;v1.0.0&lt;/code&gt; compared against Postgres &lt;code&gt;15.5&lt;/code&gt;:&lt;/p&gt;























































































































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;benchmark&lt;/th&gt;&lt;th&gt;doltgres v0.56.3&lt;/th&gt;&lt;th&gt;doltgres v1.0.0&lt;/th&gt;&lt;th&gt;postgres&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;covering_index_scan&lt;/td&gt;&lt;td&gt;6.55&lt;/td&gt;&lt;td&gt;2.48&lt;/td&gt;&lt;td&gt;18.28&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;groupby_scan&lt;/td&gt;&lt;td&gt;155.80&lt;/td&gt;&lt;td&gt;74.46&lt;/td&gt;&lt;td&gt;40.37&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;index_join&lt;/td&gt;&lt;td&gt;6.67&lt;/td&gt;&lt;td&gt;2.22&lt;/td&gt;&lt;td&gt;1.82&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;index_join_scan&lt;/td&gt;&lt;td&gt;6.21&lt;/td&gt;&lt;td&gt;1.61&lt;/td&gt;&lt;td&gt;0.69&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;index_scan&lt;/td&gt;&lt;td&gt;1235.62&lt;/td&gt;&lt;td&gt;484.44&lt;/td&gt;&lt;td&gt;183.21&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;oltp_point_select&lt;/td&gt;&lt;td&gt;0.55&lt;/td&gt;&lt;td&gt;0.37&lt;/td&gt;&lt;td&gt;0.15&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;oltp_read_only&lt;/td&gt;&lt;td&gt;11.45&lt;/td&gt;&lt;td&gt;6.55&lt;/td&gt;&lt;td&gt;2.66&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;select_random_points&lt;/td&gt;&lt;td&gt;0.97&lt;/td&gt;&lt;td&gt;0.73&lt;/td&gt;&lt;td&gt;0.22&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;select_random_ranges&lt;/td&gt;&lt;td&gt;1.20&lt;/td&gt;&lt;td&gt;1.04&lt;/td&gt;&lt;td&gt;0.42&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;table_scan&lt;/td&gt;&lt;td&gt;1235.62&lt;/td&gt;&lt;td&gt;484.44&lt;/td&gt;&lt;td&gt;183.21&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;types_table_scan&lt;/td&gt;&lt;td&gt;2778.39&lt;/td&gt;&lt;td&gt;1235.62&lt;/td&gt;&lt;td&gt;434.83&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;oltp_delete_insert&lt;/td&gt;&lt;td&gt;7.43&lt;/td&gt;&lt;td&gt;6.79&lt;/td&gt;&lt;td&gt;2.22&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;oltp_insert&lt;/td&gt;&lt;td&gt;4.10&lt;/td&gt;&lt;td&gt;3.43&lt;/td&gt;&lt;td&gt;1.10&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;oltp_read_write&lt;/td&gt;&lt;td&gt;20.00&lt;/td&gt;&lt;td&gt;13.46&lt;/td&gt;&lt;td&gt;4.41&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;oltp_update_index&lt;/td&gt;&lt;td&gt;4.10&lt;/td&gt;&lt;td&gt;3.62&lt;/td&gt;&lt;td&gt;1.14&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;oltp_update_non_index&lt;/td&gt;&lt;td&gt;3.82&lt;/td&gt;&lt;td&gt;3.30&lt;/td&gt;&lt;td&gt;1.14&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;oltp_write_only&lt;/td&gt;&lt;td&gt;8.28&lt;/td&gt;&lt;td&gt;6.91&lt;/td&gt;&lt;td&gt;1.82&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;types_delete_insert&lt;/td&gt;&lt;td&gt;7.98&lt;/td&gt;&lt;td&gt;7.17&lt;/td&gt;&lt;td&gt;2.30&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Here’s a graph of the multipliers against Postgres:
&lt;img src="https://static.dolthub.com/blogimages/doltgres_latency_summary2.png/9c879999f00d491ac8fb05ff7b7b3db15944f530d0e931d49dc3d1aa6387d82e.webp" alt="summary chart"&gt;&lt;/p&gt;
&lt;p&gt;The rest of this blog will go over every performance-related change since &lt;code&gt;v0.56.3&lt;/code&gt;.&lt;/p&gt;
&lt;h1 id="wire-format-serialization-improvements"&gt;Wire Format Serialization Improvements&lt;a class="anchor-link" aria-label="Link to heading" href="#wire-format-serialization-improvements"&gt;#&lt;/a&gt;&lt;/h1&gt;
&lt;p&gt;When comparing the flamegraphs against Dolt, the code paths surrounding wire format serialization and sending packets had the largest CPU usage discrepancy.&lt;/p&gt;
&lt;h2 id="buffered-flush"&gt;Buffered Flush&lt;a class="anchor-link" aria-label="Link to heading" href="#buffered-flush"&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Perhaps the largest performance improvement we saw was with this &lt;a href="https://github.com/dolthub/doltgresql/pull/2760"&gt;9 line change&lt;/a&gt;.
The Doltgres server needs to send result rows back the client using the Postgres protocol, which we do through the &lt;code&gt;pgproto3&lt;/code&gt; package.
Typically, these results are buffered and flushed out in large batches.
However, we were sending a packet for every individual row (including the initial row descriptor), wasting a ton of CPU cycles.
After adjusting the send logic to call &lt;code&gt;Flush()&lt;/code&gt; every &lt;code&gt;row_batch_size = 128&lt;/code&gt;, we saw drastic performance improvements practically across the board.&lt;/p&gt;
&lt;p&gt;Benchmarks that returned multiple rows saw throughput improvements of over &lt;code&gt;200%&lt;/code&gt;, with some as high as &lt;code&gt;247%&lt;/code&gt;.
You can view the full results &lt;a href="https://github.com/dolthub/doltgresql/pull/2760#issuecomment-4549939976"&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="spooling-concurrency"&gt;Spooling Concurrency&lt;a class="anchor-link" aria-label="Link to heading" href="#spooling-concurrency"&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Another improvement around sending rows back to the client is one we’ve already done &lt;a href="https://www.dolthub.com/blog/2025-12-12-how-dolt-got-as-fast-as-mysql/#balance-goroutines"&gt;before in Dolt&lt;/a&gt;.
The server handler is in charge of reading results from &lt;code&gt;RowIter&lt;/code&gt;s, converting them into the wire format, and sending these packets to the client.
Similar to the old Dolt code, we put two goroutines in charge of these steps: one to read the rows and another to convert and send the results.
The fix here is to break these steps up into three goroutines: (1) read the rows from &lt;code&gt;RowIter&lt;/code&gt;, (2) convert each row into wire format through &lt;code&gt;SQL()&lt;/code&gt; method, and (3) spool results to client.
The resulting optimization gave us over &lt;code&gt;50%&lt;/code&gt; improvement in throughput for &lt;code&gt;index_scan&lt;/code&gt;, &lt;code&gt;table_scan&lt;/code&gt;, and &lt;code&gt;types_table_scan&lt;/code&gt;; these are all benchmarks that return large result sets.&lt;/p&gt;
&lt;p&gt;You can view the full results &lt;a href="https://github.com/dolthub/doltgresql/pull/2759#issuecomment-4549612908"&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="output-function-cache"&gt;Output Function Cache&lt;a class="anchor-link" aria-label="Link to heading" href="#output-function-cache"&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The serialization format functions are fully customizable by users.
While this can be pretty useful, it requires additional complexity to be efficient.
Namely, we should not be reloading the output function every time we serialize a field, especially since it doesn’t change within a query.
So, the solution is to just &lt;a href="https://github.com/dolthub/doltgresql/pull/2956"&gt;cache it somewhere&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;This change got us &lt;code&gt;~13%&lt;/code&gt; improvement in output heavy benchmarks like &lt;code&gt;index_scan&lt;/code&gt;, &lt;code&gt;table_scan&lt;/code&gt;, and &lt;code&gt;types_table_scan&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;You can view the full results &lt;a href="https://github.com/dolthub/doltgresql/pull/2956#issuecomment-5027165226"&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="wire-format-improvements"&gt;Wire Format Improvements&lt;a class="anchor-link" aria-label="Link to heading" href="#wire-format-improvements"&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;These next optimizations all involve improving the wire format serialization itself.
Specifically, we made improvements to the &lt;code&gt;TimeOfDay&lt;/code&gt;, &lt;code&gt;bpchar&lt;/code&gt;, and &lt;code&gt;Date&lt;/code&gt; types.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;fmt.Sprintf()&lt;/code&gt; is handy but slow.
Since &lt;code&gt;TimeOfDay.String()&lt;/code&gt; produces is a small string with a strict format and known max length, we can just create a &lt;code&gt;[]byte&lt;/code&gt; and append to it.
Additionally, we can safely use the &lt;code&gt;unsafe&lt;/code&gt; package, to convert that &lt;code&gt;[]byte&lt;/code&gt; to a string.&lt;/p&gt;
&lt;pre class="astro-code github-dark" tabindex="0" data-language="go"&gt;&lt;code&gt;&lt;span class="line"&gt;&lt;span&gt;func&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;t &lt;/span&gt;&lt;span&gt;TimeOfDay&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt;() &lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;	dest &lt;/span&gt;&lt;span&gt;:=&lt;/span&gt;&lt;span&gt; make&lt;/span&gt;&lt;span&gt;([]&lt;/span&gt;&lt;span&gt;byte&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;15&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;// longest possible result is len("12:34:56.123456") = 15&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;	h, m, s, ms &lt;/span&gt;&lt;span&gt;:=&lt;/span&gt;&lt;span&gt; t.&lt;/span&gt;&lt;span&gt;Hour&lt;/span&gt;&lt;span&gt;(), t.&lt;/span&gt;&lt;span&gt;Minute&lt;/span&gt;&lt;span&gt;(), t.&lt;/span&gt;&lt;span&gt;Second&lt;/span&gt;&lt;span&gt;(), t.&lt;/span&gt;&lt;span&gt;Microsecond&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;	dest &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; append&lt;/span&gt;&lt;span&gt;(dest,&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;		'&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;'&lt;/span&gt;&lt;span&gt;+byte&lt;/span&gt;&lt;span&gt;(h&lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt;10&lt;/span&gt;&lt;span&gt;), &lt;/span&gt;&lt;span&gt;'&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;'&lt;/span&gt;&lt;span&gt;+byte&lt;/span&gt;&lt;span&gt;(h&lt;/span&gt;&lt;span&gt;%&lt;/span&gt;&lt;span&gt;10&lt;/span&gt;&lt;span&gt;), &lt;/span&gt;&lt;span&gt;'&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt;'&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;		'&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;'&lt;/span&gt;&lt;span&gt;+byte&lt;/span&gt;&lt;span&gt;(m&lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt;10&lt;/span&gt;&lt;span&gt;), &lt;/span&gt;&lt;span&gt;'&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;'&lt;/span&gt;&lt;span&gt;+byte&lt;/span&gt;&lt;span&gt;(m&lt;/span&gt;&lt;span&gt;%&lt;/span&gt;&lt;span&gt;10&lt;/span&gt;&lt;span&gt;), &lt;/span&gt;&lt;span&gt;'&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt;'&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;		'&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;'&lt;/span&gt;&lt;span&gt;+byte&lt;/span&gt;&lt;span&gt;(s&lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt;10&lt;/span&gt;&lt;span&gt;), &lt;/span&gt;&lt;span&gt;'&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;'&lt;/span&gt;&lt;span&gt;+byte&lt;/span&gt;&lt;span&gt;(s&lt;/span&gt;&lt;span&gt;%&lt;/span&gt;&lt;span&gt;10&lt;/span&gt;&lt;span&gt;))&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;	if&lt;/span&gt;&lt;span&gt; ms &lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;span&gt; 0&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;		dest &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; append&lt;/span&gt;&lt;span&gt;(dest, &lt;/span&gt;&lt;span&gt;'&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;'&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;		cmp &lt;/span&gt;&lt;span&gt;:=&lt;/span&gt;&lt;span&gt; 100_000&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;		for&lt;/span&gt;&lt;span&gt; cmp &lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;span&gt; 0&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;			dest &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; append&lt;/span&gt;&lt;span&gt;(dest, &lt;/span&gt;&lt;span&gt;'&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;'&lt;/span&gt;&lt;span&gt;+byte&lt;/span&gt;&lt;span&gt;(ms&lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt;cmp))&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;			ms &lt;/span&gt;&lt;span&gt;%=&lt;/span&gt;&lt;span&gt; cmp&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;			cmp &lt;/span&gt;&lt;span&gt;/=&lt;/span&gt;&lt;span&gt; 10&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;		}&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;		// trim trailing 0s&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;		for&lt;/span&gt;&lt;span&gt; i &lt;/span&gt;&lt;span&gt;:=&lt;/span&gt;&lt;span&gt; len&lt;/span&gt;&lt;span&gt;(dest) &lt;/span&gt;&lt;span&gt;-&lt;/span&gt;&lt;span&gt; 1&lt;/span&gt;&lt;span&gt;; i &lt;/span&gt;&lt;span&gt;&gt;=&lt;/span&gt;&lt;span&gt; 0&lt;/span&gt;&lt;span&gt;; i&lt;/span&gt;&lt;span&gt;--&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;			if&lt;/span&gt;&lt;span&gt; dest[i] &lt;/span&gt;&lt;span&gt;!=&lt;/span&gt;&lt;span&gt; '&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;'&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;				dest &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; dest[:i&lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;				break&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;			}&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;		}&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;	}&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;	return&lt;/span&gt;&lt;span&gt; unsafe.&lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt;(unsafe.&lt;/span&gt;&lt;span&gt;SliceData&lt;/span&gt;&lt;span&gt;(dest), &lt;/span&gt;&lt;span&gt;len&lt;/span&gt;&lt;span&gt;(dest))&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Next, we made an improvement to &lt;code&gt;truncateString()&lt;/code&gt; for &lt;code&gt;bpchar&lt;/code&gt; by again writing our own implementation.
The old implementation used the &lt;code&gt;utf8&lt;/code&gt; package to get the rune length and decode each rune.&lt;/p&gt;
&lt;pre class="astro-code github-dark" tabindex="0" data-language="go"&gt;&lt;code&gt;&lt;span class="line"&gt;&lt;span&gt;func&lt;/span&gt;&lt;span&gt; truncateString&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;val&lt;/span&gt;&lt;span&gt; string&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;runeLimit&lt;/span&gt;&lt;span&gt; int32&lt;/span&gt;&lt;span&gt;) (&lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;int32&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;	runeLength &lt;/span&gt;&lt;span&gt;:=&lt;/span&gt;&lt;span&gt; int32&lt;/span&gt;&lt;span&gt;(utf8.&lt;/span&gt;&lt;span&gt;RuneCountInString&lt;/span&gt;&lt;span&gt;(val))&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;	if&lt;/span&gt;&lt;span&gt; runeLength &lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;span&gt; runeLimit {&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;		startString &lt;/span&gt;&lt;span&gt;:=&lt;/span&gt;&lt;span&gt; val&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;		for&lt;/span&gt;&lt;span&gt; i &lt;/span&gt;&lt;span&gt;:=&lt;/span&gt;&lt;span&gt; int32&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;); i &lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt; runeLimit; i&lt;/span&gt;&lt;span&gt;++&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;			_, size &lt;/span&gt;&lt;span&gt;:=&lt;/span&gt;&lt;span&gt; utf8.&lt;/span&gt;&lt;span&gt;DecodeRuneInString&lt;/span&gt;&lt;span&gt;(val)&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;			val &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; val[size:]&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;		}&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;		return&lt;/span&gt;&lt;span&gt; startString[:&lt;/span&gt;&lt;span&gt;len&lt;/span&gt;&lt;span&gt;(startString)&lt;/span&gt;&lt;span&gt;-&lt;/span&gt;&lt;span&gt;len&lt;/span&gt;&lt;span&gt;(val)], runeLength&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;	}&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;	return&lt;/span&gt;&lt;span&gt; val, runeLength&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here’s a Golang fun fact: &lt;code&gt;len(string)&lt;/code&gt; returns the number of bytes, while &lt;code&gt;for pos := range string&lt;/code&gt; returns start indexes of each rune.
Using this, we can rewrite the same logic like so:&lt;/p&gt;
&lt;pre class="astro-code github-dark" tabindex="0" data-language="go"&gt;&lt;code&gt;&lt;span class="line"&gt;&lt;span&gt;func&lt;/span&gt;&lt;span&gt; truncateString&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;val&lt;/span&gt;&lt;span&gt; string&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;runeLimit&lt;/span&gt;&lt;span&gt; int32&lt;/span&gt;&lt;span&gt;) (&lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;int32&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;	var&lt;/span&gt;&lt;span&gt; n &lt;/span&gt;&lt;span&gt;int32&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;	for&lt;/span&gt;&lt;span&gt; pos &lt;/span&gt;&lt;span&gt;:=&lt;/span&gt;&lt;span&gt; range&lt;/span&gt;&lt;span&gt; val {&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;		if&lt;/span&gt;&lt;span&gt; n &lt;/span&gt;&lt;span&gt;&gt;=&lt;/span&gt;&lt;span&gt; runeLimit {&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;			return&lt;/span&gt;&lt;span&gt; val[:pos], n&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;		}&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;		n&lt;/span&gt;&lt;span&gt;++&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;	}&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;	return&lt;/span&gt;&lt;span&gt; val, n&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This avoids iterating over the string twice and avoids extra string slicing logic.&lt;/p&gt;
&lt;p&gt;You can view the full results &lt;a href="https://github.com/dolthub/doltgresql/pull/2799#issuecomment-4607972773"&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Doltgres allows users to change the format of the date output through session variables.
So during the wire formatting stage, we must call &lt;code&gt;GetDateStyleOutputFormat()&lt;/code&gt;, which acquires a mutex and does a bunch of string operations.
The &lt;code&gt;DATE&lt;/code&gt; style shouldn’t change within a query, so it makes no sense to do all these steps for each row and each &lt;code&gt;DATE&lt;/code&gt; field.
Caching the format in the context, netted us &lt;code&gt;10%-14%&lt;/code&gt; improvement on the &lt;code&gt;index_scan&lt;/code&gt;, &lt;code&gt;table_scan&lt;/code&gt;, and &lt;code&gt;types_table_scan&lt;/code&gt; benchmarks.&lt;/p&gt;
&lt;p&gt;You can view the full results &lt;a href="https://github.com/dolthub/doltgresql/pull/2895#issuecomment-4859521741"&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Lastly, we made the &lt;code&gt;*_scan&lt;/code&gt; benchmarks a faster by adjusting &lt;a href="https://github.com/dolthub/doltgresql/pull/2932"&gt;this single line&lt;/a&gt;.&lt;/p&gt;
&lt;pre class="astro-code github-dark" tabindex="0" data-language="diff"&gt;&lt;code&gt;&lt;span class="line"&gt;&lt;span&gt;&lt;span&gt;-&lt;/span&gt;return sqltypes.MakeTrusted(sqltypes.Text, types.AppendAndSliceString(dest, value)), nil&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;&lt;span&gt;+&lt;/span&gt;return sqltypes.MakeTrusted(sqltypes.Text, encodings.StringToBytes(value)), nil&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This was a relic from copying over code from Dolt, where the &lt;code&gt;SQL()&lt;/code&gt; methods are able to share a large byte buffer to reduce memory allocations.
This isn’t possible in Doltgres, so appending &lt;code&gt;value&lt;/code&gt; to &lt;code&gt;dest&lt;/code&gt; here is just wasting a copy operation as we can just use &lt;code&gt;value&lt;/code&gt; directly.
As a result, &lt;code&gt;index_scan&lt;/code&gt;, &lt;code&gt;table_scan&lt;/code&gt; and &lt;code&gt;types_table_scan&lt;/code&gt; saw a &lt;code&gt;8%-13%&lt;/code&gt; improvement.&lt;/p&gt;
&lt;p&gt;You can view the full results &lt;a href="https://github.com/dolthub/doltgresql/pull/2932#issuecomment-4973083155"&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;h1 id="collections"&gt;Collections&lt;a class="anchor-link" aria-label="Link to heading" href="#collections"&gt;#&lt;/a&gt;&lt;/h1&gt;
&lt;p&gt;Doltgres implements &lt;code&gt;Collections&lt;/code&gt; which hold both built-in and user-defined functions, views, procedures, types, etc.
However, we were being very inefficient by loading these collections every query…sometimes multiple times!
On our first pass, we cached &lt;code&gt;Collections&lt;/code&gt; &lt;a href="https://github.com/dolthub/doltgresql/pull/2774"&gt;per query&lt;/a&gt;, reducing the number of loads.
This gave us a decent performance bump across all the &lt;a href="https://github.com/dolthub/doltgresql/pull/2774#issuecomment-4569265797"&gt;benchmarks&lt;/a&gt; (around &lt;code&gt;5%-12%&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;Later on, we improved this further by &lt;a href="https://github.com/dolthub/doltgresql/pull/3024"&gt;simplifying &lt;code&gt;Collections&lt;/code&gt; storage&lt;/a&gt;.
This resulted in another decent bump across the board, so around &lt;code&gt;5%-15%&lt;/code&gt; improvement.&lt;/p&gt;
&lt;p&gt;You can view the full results &lt;a href="https://github.com/dolthub/doltgresql/pull/3024#issuecomment-5180693809"&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;h1 id="analyzer-improvements"&gt;Analyzer Improvements&lt;a class="anchor-link" aria-label="Link to heading" href="#analyzer-improvements"&gt;#&lt;/a&gt;&lt;/h1&gt;
&lt;p&gt;The analyzer is an essential component to improving the query performance in Doltgres, and ours could use a lot of improving.
Here is some of the work we did to analysis and costing that has led to better latency in Doltgres (and Dolt).&lt;/p&gt;
&lt;h2 id="fix-covering-indexes"&gt;Fix Covering Indexes&lt;a class="anchor-link" aria-label="Link to heading" href="#fix-covering-indexes"&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;This performance optimization is actually bug fix and the first Doltgres performance improvement I made chronologically.
Dolt was missing a case for &lt;code&gt;sql.ExtendedType&lt;/code&gt;, which Doltgres explicitly uses, so indexes weren’t getting used properly.
Afterwards, we received a nice &lt;code&gt;21.5%&lt;/code&gt; bump in throughput for &lt;code&gt;covering_index_scan&lt;/code&gt; and &lt;code&gt;11.5%&lt;/code&gt; for &lt;code&gt;select_random_ranges&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;You can view the full results &lt;a href="https://github.com/dolthub/doltgresql/pull/2758#issuecomment-4548913455"&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="no-index-groupby"&gt;No Index GroupBy&lt;a class="anchor-link" aria-label="Link to heading" href="#no-index-groupby"&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;After some investigation, it appears that sometimes it is better to perform a full table scan rather than lookups through a secondary index.
This is because non-covering secondary indexes perform two lookups, making them expensive for filters with low selectivity.
We adjusted our coster to include a full table scan option when assigning indexes and added some heuristics.&lt;/p&gt;
&lt;p&gt;As a result, median latency on the &lt;code&gt;groupby_scan&lt;/code&gt; benchmark saw a &lt;code&gt;43.80%&lt;/code&gt; improvement.
You can view the full results &lt;a href="https://github.com/dolthub/doltgresql/pull/3001#issuecomment-5135634713"&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;We have previously discussed this optimization in greater detailed in this &lt;a href="https://www.dolthub.com/blog/2026-08-03-no-index-groupby/"&gt;blog&lt;/a&gt;.&lt;/p&gt;
&lt;h1 id="conclusion"&gt;Conclusion&lt;a class="anchor-link" aria-label="Link to heading" href="#conclusion"&gt;#&lt;/a&gt;&lt;/h1&gt;
&lt;p&gt;For fun, here are all the latency benchmarks we have compared against Postgres:&lt;/p&gt;













































































































































































































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;test&lt;/th&gt;&lt;th&gt;dolt&lt;/th&gt;&lt;th&gt;doltgres&lt;/th&gt;&lt;th&gt;mysql&lt;/th&gt;&lt;th&gt;postgres&lt;/th&gt;&lt;th&gt;dolt vs postgres&lt;/th&gt;&lt;th&gt;doltgres vs postgres&lt;/th&gt;&lt;th&gt;mysql vs postgres&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;covering_index_scan&lt;/td&gt;&lt;td&gt;2.35&lt;/td&gt;&lt;td&gt;2.48&lt;/td&gt;&lt;td&gt;17.32&lt;/td&gt;&lt;td&gt;18.28&lt;/td&gt;&lt;td&gt;0.13&lt;/td&gt;&lt;td&gt;0.14&lt;/td&gt;&lt;td&gt;0.95&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;groupby_scan&lt;/td&gt;&lt;td&gt;63.32&lt;/td&gt;&lt;td&gt;74.46&lt;/td&gt;&lt;td&gt;134.90&lt;/td&gt;&lt;td&gt;40.37&lt;/td&gt;&lt;td&gt;1.57&lt;/td&gt;&lt;td&gt;1.84&lt;/td&gt;&lt;td&gt;3.34&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;index_join&lt;/td&gt;&lt;td&gt;1.93&lt;/td&gt;&lt;td&gt;2.22&lt;/td&gt;&lt;td&gt;3.43&lt;/td&gt;&lt;td&gt;1.82&lt;/td&gt;&lt;td&gt;1.06&lt;/td&gt;&lt;td&gt;1.22&lt;/td&gt;&lt;td&gt;1.88&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;index_join_scan&lt;/td&gt;&lt;td&gt;1.32&lt;/td&gt;&lt;td&gt;1.61&lt;/td&gt;&lt;td&gt;4.25&lt;/td&gt;&lt;td&gt;0.69&lt;/td&gt;&lt;td&gt;1.91&lt;/td&gt;&lt;td&gt;2.33&lt;/td&gt;&lt;td&gt;6.16&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;index_scan&lt;/td&gt;&lt;td&gt;196.89&lt;/td&gt;&lt;td&gt;484.44&lt;/td&gt;&lt;td&gt;344.08&lt;/td&gt;&lt;td&gt;183.21&lt;/td&gt;&lt;td&gt;1.07&lt;/td&gt;&lt;td&gt;2.64&lt;/td&gt;&lt;td&gt;1.88&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;oltp_point_select&lt;/td&gt;&lt;td&gt;0.25&lt;/td&gt;&lt;td&gt;0.37&lt;/td&gt;&lt;td&gt;0.19&lt;/td&gt;&lt;td&gt;0.15&lt;/td&gt;&lt;td&gt;1.67&lt;/td&gt;&lt;td&gt;2.47&lt;/td&gt;&lt;td&gt;1.27&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;oltp_read_only&lt;/td&gt;&lt;td&gt;5.00&lt;/td&gt;&lt;td&gt;6.55&lt;/td&gt;&lt;td&gt;3.62&lt;/td&gt;&lt;td&gt;2.66&lt;/td&gt;&lt;td&gt;1.88&lt;/td&gt;&lt;td&gt;2.46&lt;/td&gt;&lt;td&gt;1.36&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;select_random_points&lt;/td&gt;&lt;td&gt;0.52&lt;/td&gt;&lt;td&gt;0.73&lt;/td&gt;&lt;td&gt;0.35&lt;/td&gt;&lt;td&gt;0.22&lt;/td&gt;&lt;td&gt;2.36&lt;/td&gt;&lt;td&gt;3.32&lt;/td&gt;&lt;td&gt;1.59&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;select_random_ranges&lt;/td&gt;&lt;td&gt;0.64&lt;/td&gt;&lt;td&gt;1.04&lt;/td&gt;&lt;td&gt;0.38&lt;/td&gt;&lt;td&gt;0.42&lt;/td&gt;&lt;td&gt;1.52&lt;/td&gt;&lt;td&gt;2.48&lt;/td&gt;&lt;td&gt;0.90&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;table_scan&lt;/td&gt;&lt;td&gt;196.89&lt;/td&gt;&lt;td&gt;484.44&lt;/td&gt;&lt;td&gt;344.08&lt;/td&gt;&lt;td&gt;183.21&lt;/td&gt;&lt;td&gt;1.07&lt;/td&gt;&lt;td&gt;2.64&lt;/td&gt;&lt;td&gt;1.88&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;types_table_scan&lt;/td&gt;&lt;td&gt;442.73&lt;/td&gt;&lt;td&gt;1235.62&lt;/td&gt;&lt;td&gt;746.32&lt;/td&gt;&lt;td&gt;434.83&lt;/td&gt;&lt;td&gt;1.02&lt;/td&gt;&lt;td&gt;2.84&lt;/td&gt;&lt;td&gt;1.72&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;oltp_delete_insert&lt;/td&gt;&lt;td&gt;6.21&lt;/td&gt;&lt;td&gt;6.79&lt;/td&gt;&lt;td&gt;7.70&lt;/td&gt;&lt;td&gt;2.22&lt;/td&gt;&lt;td&gt;2.80&lt;/td&gt;&lt;td&gt;3.06&lt;/td&gt;&lt;td&gt;3.47&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;oltp_insert&lt;/td&gt;&lt;td&gt;3.13&lt;/td&gt;&lt;td&gt;3.43&lt;/td&gt;&lt;td&gt;4.03&lt;/td&gt;&lt;td&gt;1.10&lt;/td&gt;&lt;td&gt;2.85&lt;/td&gt;&lt;td&gt;3.12&lt;/td&gt;&lt;td&gt;3.66&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;oltp_read_write&lt;/td&gt;&lt;td&gt;11.24&lt;/td&gt;&lt;td&gt;13.46&lt;/td&gt;&lt;td&gt;8.90&lt;/td&gt;&lt;td&gt;4.41&lt;/td&gt;&lt;td&gt;2.55&lt;/td&gt;&lt;td&gt;3.05&lt;/td&gt;&lt;td&gt;2.02&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;oltp_update_index&lt;/td&gt;&lt;td&gt;3.30&lt;/td&gt;&lt;td&gt;3.62&lt;/td&gt;&lt;td&gt;4.33&lt;/td&gt;&lt;td&gt;1.14&lt;/td&gt;&lt;td&gt;2.89&lt;/td&gt;&lt;td&gt;3.18&lt;/td&gt;&lt;td&gt;3.80&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;oltp_update_non_index&lt;/td&gt;&lt;td&gt;3.02&lt;/td&gt;&lt;td&gt;3.30&lt;/td&gt;&lt;td&gt;4.10&lt;/td&gt;&lt;td&gt;1.14&lt;/td&gt;&lt;td&gt;2.65&lt;/td&gt;&lt;td&gt;2.89&lt;/td&gt;&lt;td&gt;3.60&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;oltp_write_only&lt;/td&gt;&lt;td&gt;6.21&lt;/td&gt;&lt;td&gt;6.91&lt;/td&gt;&lt;td&gt;5.18&lt;/td&gt;&lt;td&gt;1.82&lt;/td&gt;&lt;td&gt;3.41&lt;/td&gt;&lt;td&gt;3.80&lt;/td&gt;&lt;td&gt;2.85&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;types_delete_insert&lt;/td&gt;&lt;td&gt;6.79&lt;/td&gt;&lt;td&gt;7.17&lt;/td&gt;&lt;td&gt;8.28&lt;/td&gt;&lt;td&gt;2.30&lt;/td&gt;&lt;td&gt;2.95&lt;/td&gt;&lt;td&gt;3.12&lt;/td&gt;&lt;td&gt;3.60&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;avg_mult&lt;/td&gt;&lt;td&gt;&lt;/td&gt;&lt;td&gt;&lt;/td&gt;&lt;td&gt;&lt;/td&gt;&lt;td&gt;&lt;/td&gt;&lt;td&gt;1.92&lt;/td&gt;&lt;td&gt;2.59&lt;/td&gt;&lt;td&gt;2.55&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Using some old tricks and writing some new ones, we’ve trimmed a lot of fat from Doltgres; in fact, we’re about as lean as MySQL (which is apparently not that lean standing next to Postgres).
We are still &lt;code&gt;2.6x&lt;/code&gt; slower than Postgres, so we have a long ways to go.
Dolt stands at &lt;code&gt;1.92x&lt;/code&gt;, so the theoretical best Doltgres can do (as far as we know) is under &lt;code&gt;2x&lt;/code&gt;.
Feel free to chat with us on &lt;a href="https://discord.gg/gqr7K4VNKe"&gt;Discord&lt;/a&gt; or file a &lt;a href="https://github.com/dolthub/dolt/issues"&gt;Github issue&lt;/a&gt;.&lt;/p&gt;</content:encoded>
      <dc:creator>James Cor</dc:creator>
      <category>technical</category>
      <category>performance</category>
    </item>
    <item>
      <title>Doltgres 1.0</title>
      <link>https://dolthub.com/blog/2026-08-06-doltgres-1-0/</link>
      <guid isPermaLink="true">https://dolthub.com/blog/2026-08-06-doltgres-1-0/</guid>
      <description>Doltgres, the Postgres-flavored version of Dolt, is now 1.0 and ready for production.</description>
      <pubDate>Thu, 06 Aug 2026 00:00:00 GMT</pubDate>
      <content:encoded>&lt;p&gt;At DoltHub, we’ve been in the version-controlled database business for eight years now. We made
&lt;a href="https://doltdb.com"&gt;Dolt&lt;/a&gt;, the world’s first version-controlled SQL database, and after a few years
in beta it hit 1.0 in 2023. It’s now running on thousands of machines around the world every
day, powering as many interesting version-controlled applications.&lt;/p&gt;
&lt;p&gt;Since we first announced Dolt, we’ve gotten one question more than any other:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;What about a Postgres version?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Dolt speaks the MySQL dialect and wire protocol, so it’s compatible with any tool or library that
can connect to MySQL. At the time we started writing the database, MySQL was far and away the most
popular free SQL database, but that was already starting to change. Postgres was gaining ground and
continues to do so. Today, few companies are building new products on top of MySQL. Nearly everyone
chooses Postgres for new work. This means that most of the new generation of database application
engineers are familiar with Postgres’s SQL dialect, toolchain, and ecosystem.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://doltgres.com/"&gt;Doltgres&lt;/a&gt; is our way of meeting those customers where they live. Doltgres
combines the performant Git-style version control that makes Dolt unique with full Postgres
compatibility. Your favorite Postgres workbench or connection library can connect to Doltgres just
like a normal Postgres database, but with extra &lt;a href="https://www.doltgres.com/docs/reference/version-control/dolt-system-tables/"&gt;system
tables&lt;/a&gt; and
&lt;a href="https://www.doltgres.com/docs/reference/version-control/dolt-sql-functions/"&gt;functions&lt;/a&gt; for full
version control of all schema and data.&lt;/p&gt;
&lt;p&gt;It’s been almost three years since we announced &lt;a href="https://www.dolthub.com/blog/2023-11-01-announcing-doltgresql/"&gt;Doltgres’s alpha
release&lt;/a&gt; and about 18 months since
we announced &lt;a href="https://www.dolthub.com/blog/2025-04-16-doltgres-goes-beta/"&gt;Doltgres Beta&lt;/a&gt;. Today is
the 8th anniversary of DoltHub Inc., and we’re excited to announce that Doltgres has finally achieved
its 1.0 release.&lt;/p&gt;
&lt;h1 id="what-does-10-mean"&gt;What does 1.0 mean?&lt;a class="anchor-link" aria-label="Link to heading" href="#what-does-10-mean"&gt;#&lt;/a&gt;&lt;/h1&gt;
&lt;p&gt;Doltgres 1.0 means four things.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Forward Storage Compatibility&lt;/li&gt;
&lt;li&gt;Production Performance&lt;/li&gt;
&lt;li&gt;Postgres Compatibility&lt;/li&gt;
&lt;li&gt;Stable Version Control Interface&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="forward-storage-compatibility"&gt;Forward Storage Compatibility&lt;a class="anchor-link" aria-label="Link to heading" href="#forward-storage-compatibility"&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;All future 1.x versions of Doltgres will be backwards compatible with 1.0.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Dolt’s storage engine has gone through a couple of backwards-incompatible changes since its
original beta, including one that required existing customers to migrate their data. Doltgres has
likewise explored different ways of storing and versioning its data leading up to the 1.0 release,
but managed to do so in a way that didn’t require any customer data migration.&lt;/p&gt;
&lt;p&gt;With Doltgres 1.0 we are committing to the current storage format used for all data. There will be
no backwards-incompatible storage changes in any 1.x release of Doltgres.&lt;/p&gt;
&lt;h2 id="production-performance"&gt;Production Performance&lt;a class="anchor-link" aria-label="Link to heading" href="#production-performance"&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;Doltgres 1.0 has production level query performance.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;a href="https://www.dolthub.com/blog/2025-12-04-dolt-is-as-fast-as-mysql/"&gt;Dolt is faster than MySQL&lt;/a&gt; on
the standard sysbench performance suite. Doltgres uses Dolt’s underlying storage and query engine,
so that same performance applies to Doltgres as well.&lt;/p&gt;
&lt;p&gt;However, MySQL itself is roughly 2-3 times slower than Postgres. This means that &lt;a href="https://www.doltgres.com/docs/reference/benchmarks/latency/"&gt;Doltgres is about
2.7 times slower than Postgres&lt;/a&gt; as of
this launch. But there are two pieces of good news here. First, databases are very fast. We’re
talking about the difference between 0.2 milliseconds for a query versus 0.5 milliseconds. In
practice, any difference in performance for typical OLTP queries in a database-backed application is
dominated by network latency, with the database itself being a relatively small part of overall
measured latency. Second, we’re continuing to make improvements to Dolt’s performance over time. We
started out 5x slower than MySQL and have now passed it in performance.&lt;/p&gt;
&lt;p&gt;Doltgres is slower than Postgres but still more than fast enough to handle your production
workload. And it will only get faster in future releases.&lt;/p&gt;
&lt;h2 id="postgres-compatibility"&gt;Postgres Compatibility&lt;a class="anchor-link" aria-label="Link to heading" href="#postgres-compatibility"&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;Doltgres 1.0 is 99% Postgres compatible.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Doltgres measures Postgres compatibility in two primary ways:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A test suite of 5.6 million SQL queries that are verified against real Postgres for
correctness. These originally come from the
&lt;a href="https://sqlite.org/sqllogictest/doc/trunk/about.wiki"&gt;sqllogictest&lt;/a&gt; project, which was developed
for SQLite3. Doltgres currently scores over 99% on this suite.&lt;/li&gt;
&lt;li&gt;Comprehensive integration tests of different client libraries in different languages. Currently
&lt;a href="https://www.doltgres.com/docs/reference/supported-clients/clients/#supported-clients"&gt;over 20 have official
support&lt;/a&gt;,
with more being added all the time.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Compatibility has been our highest priority for the last year of development. We have imported many
hundreds of real-world Postgres dumps and written tests for dozens of client libraries. This means
we’re very confident your Postgres schema and queries will work in Doltgres. If you find that they
don’t, we promise to &lt;a href="https://www.dolthub.com/blog/2024-05-15-24-hour-bug-fixes/"&gt;fix the problem in 24
hours&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="stable-version-control-interface"&gt;Stable Version Control Interface&lt;a class="anchor-link" aria-label="Link to heading" href="#stable-version-control-interface"&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;Doltgres 1.0 version control interfaces will not change.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The &lt;a href="https://www.doltgres.com/docs/reference/version-control/dolt-system-tables/"&gt;system tables&lt;/a&gt; and
&lt;a href="https://www.doltgres.com/docs/reference/version-control/dolt-sql-functions/"&gt;functions&lt;/a&gt; that
implement the version control features of Doltgres have been battle-hardened by years of production
use in Dolt. You can write applications that use them and be assured they will not change in any 1.x
release of Doltgres.&lt;/p&gt;
&lt;p&gt;We’ll of course continue innovating by adding new version control features and making the existing
ones work better and faster. But the code and queries you write today will continue working for
every 1.x release.&lt;/p&gt;
&lt;h1 id="whats-next"&gt;What’s next?&lt;a class="anchor-link" aria-label="Link to heading" href="#whats-next"&gt;#&lt;/a&gt;&lt;/h1&gt;
&lt;p&gt;Doltgres 1.0 is just the beginning. You can read &lt;a href="https://www.dolthub.com/docs/other/roadmap/"&gt;our
roadmap&lt;/a&gt; for details, but here are some highlights.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Support for common extensions including PostGIS&lt;/li&gt;
&lt;li&gt;Vector indexes&lt;/li&gt;
&lt;li&gt;Row-level security&lt;/li&gt;
&lt;li&gt;Collation support&lt;/li&gt;
&lt;li&gt;Better DDL support&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Paying customers get write access to the roadmap.&lt;/p&gt;
&lt;h1 id="conclusion"&gt;Conclusion&lt;a class="anchor-link" aria-label="Link to heading" href="#conclusion"&gt;#&lt;/a&gt;&lt;/h1&gt;
&lt;p&gt;If you’ve been interested in a version-controlled SQL database but have been waiting for the
Postgres version, this is your signal to &lt;a href="https://github.com/dolthub/doltgresql/releases/latest"&gt;try it out
now&lt;/a&gt;. The current version has been years in
the making and represents a lot of hard work and innovation by the team. We’re very excited for you
to try it and to tell us what you think.&lt;/p&gt;
&lt;p&gt;Questions about using the 1.0 release? Find a bug you want fixed? Come by our
&lt;a href="https://discord.gg/gqr7K4VNKe"&gt;Discord&lt;/a&gt; to talk to our engineering team and meet other Doltgres
users.&lt;/p&gt;</content:encoded>
      <dc:creator>Zach Musgrave</dc:creator>
      <category>doltgres</category>
      <category>feature release</category>
    </item>
    <item>
      <title>No Index GroupBy Optimization</title>
      <link>https://dolthub.com/blog/2026-08-03-no-index-groupby/</link>
      <guid isPermaLink="true">https://dolthub.com/blog/2026-08-03-no-index-groupby/</guid>
      <description>Sneak peek at one of the optimizations applied to Dolt and Doltgres</description>
      <pubDate>Mon, 03 Aug 2026 00:00:00 GMT</pubDate>
      <content:encoded>&lt;p&gt;You may have heard that we’re launching &lt;a href="https://www.dolthub.com/blog/2026-06-26-doltgres-1-0-coming-this-fall/"&gt;Doltgres 1.0&lt;/a&gt; on August 6th.
As part of the launch, we’ve been improving Doltgres’s performance on &lt;a href="https://www.doltgres.com/docs/reference/benchmarks/latency/"&gt;Sysbench Latency&lt;/a&gt;.
This blog focuses on one of the optimizations we made, specifically focusing on &lt;code&gt;groupby_scan&lt;/code&gt;.&lt;/p&gt;
&lt;h1 id="discovering-the-optimization"&gt;Discovering the Optimization&lt;a class="anchor-link" aria-label="Link to heading" href="#discovering-the-optimization"&gt;#&lt;/a&gt;&lt;/h1&gt;
&lt;p&gt;Doltgres reuses large portions of Dolt and GMS, so it’s not unreasonable to assume that Doltgres should perform similarly to Dolt.
Comparing latencies across the different platforms helps us highlight where we are underperforming and where we should focus our attention.&lt;/p&gt;










































































































































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;benchmark&lt;/th&gt;&lt;th&gt;dolt&lt;/th&gt;&lt;th&gt;doltgres&lt;/th&gt;&lt;th&gt;mysql&lt;/th&gt;&lt;th&gt;postgres&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;covering_index_scan&lt;/td&gt;&lt;td&gt;2.35&lt;/td&gt;&lt;td&gt;2.48&lt;/td&gt;&lt;td&gt;17.01&lt;/td&gt;&lt;td&gt;17.95&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;groupby_scan&lt;/td&gt;&lt;td&gt;144.96&lt;/td&gt;&lt;td&gt;147.61&lt;/td&gt;&lt;td&gt;144.97&lt;/td&gt;&lt;td&gt;40.37&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;index_join&lt;/td&gt;&lt;td&gt;1.93&lt;/td&gt;&lt;td&gt;2.30&lt;/td&gt;&lt;td&gt;3.43&lt;/td&gt;&lt;td&gt;1.82&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;index_join_scan&lt;/td&gt;&lt;td&gt;1.32&lt;/td&gt;&lt;td&gt;1.70&lt;/td&gt;&lt;td&gt;4.18&lt;/td&gt;&lt;td&gt;0.67&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;index_scan&lt;/td&gt;&lt;td&gt;219.36&lt;/td&gt;&lt;td&gt;493.24&lt;/td&gt;&lt;td&gt;350.33&lt;/td&gt;&lt;td&gt;179.94&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;oltp_point_select&lt;/td&gt;&lt;td&gt;0.25&lt;/td&gt;&lt;td&gt;0.39&lt;/td&gt;&lt;td&gt;0.19&lt;/td&gt;&lt;td&gt;0.15&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;oltp_read_only&lt;/td&gt;&lt;td&gt;5.00&lt;/td&gt;&lt;td&gt;6.79&lt;/td&gt;&lt;td&gt;3.68&lt;/td&gt;&lt;td&gt;2.66&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;select_random_points&lt;/td&gt;&lt;td&gt;0.52&lt;/td&gt;&lt;td&gt;0.80&lt;/td&gt;&lt;td&gt;0.36&lt;/td&gt;&lt;td&gt;0.22&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;select_random_ranges&lt;/td&gt;&lt;td&gt;0.65&lt;/td&gt;&lt;td&gt;1.21&lt;/td&gt;&lt;td&gt;0.39&lt;/td&gt;&lt;td&gt;0.42&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;table_scan&lt;/td&gt;&lt;td&gt;207.82&lt;/td&gt;&lt;td&gt;475.79&lt;/td&gt;&lt;td&gt;350.33&lt;/td&gt;&lt;td&gt;179.94&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;types_table_scan&lt;/td&gt;&lt;td&gt;458.96&lt;/td&gt;&lt;td&gt;1213.57&lt;/td&gt;&lt;td&gt;759.88&lt;/td&gt;&lt;td&gt;427.07&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;oltp_delete_insert&lt;/td&gt;&lt;td&gt;6.21&lt;/td&gt;&lt;td&gt;6.91&lt;/td&gt;&lt;td&gt;7.70&lt;/td&gt;&lt;td&gt;2.22&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;oltp_insert&lt;/td&gt;&lt;td&gt;3.19&lt;/td&gt;&lt;td&gt;3.89&lt;/td&gt;&lt;td&gt;4.10&lt;/td&gt;&lt;td&gt;1.10&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;oltp_read_write&lt;/td&gt;&lt;td&gt;11.24&lt;/td&gt;&lt;td&gt;14.46&lt;/td&gt;&lt;td&gt;8.90&lt;/td&gt;&lt;td&gt;4.33&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;oltp_update_index&lt;/td&gt;&lt;td&gt;3.30&lt;/td&gt;&lt;td&gt;3.82&lt;/td&gt;&lt;td&gt;4.41&lt;/td&gt;&lt;td&gt;1.14&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;oltp_update_non_index&lt;/td&gt;&lt;td&gt;3.02&lt;/td&gt;&lt;td&gt;3.55&lt;/td&gt;&lt;td&gt;4.18&lt;/td&gt;&lt;td&gt;1.12&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;oltp_write_only&lt;/td&gt;&lt;td&gt;6.32&lt;/td&gt;&lt;td&gt;7.43&lt;/td&gt;&lt;td&gt;5.18&lt;/td&gt;&lt;td&gt;1.79&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;types_delete_insert&lt;/td&gt;&lt;td&gt;6.79&lt;/td&gt;&lt;td&gt;7.56&lt;/td&gt;&lt;td&gt;8.43&lt;/td&gt;&lt;td&gt;2.30&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Focusing on &lt;code&gt;groupby_scan&lt;/code&gt;, we see that Postgres is somehow outperforming Dolt, Doltgres, and even MySQL by a significant margin; it’s over &lt;code&gt;3x&lt;/code&gt; faster.
To see what they were doing, I ran &lt;code&gt;EXPLAIN&lt;/code&gt; on each of the databases.&lt;/p&gt;
&lt;p&gt;Dolt:&lt;/p&gt;
&lt;pre class="astro-code github-dark" tabindex="0" data-language="sql"&gt;&lt;code&gt;&lt;span class="line"&gt;&lt;span&gt;sbtest&lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt;main&lt;/span&gt;&lt;span&gt;*&gt;&lt;/span&gt;&lt;span&gt; explain plan &lt;/span&gt;&lt;span&gt;SELECT&lt;/span&gt;&lt;span&gt; year_col, &lt;/span&gt;&lt;span&gt;count&lt;/span&gt;&lt;span&gt;(year_col), &lt;/span&gt;&lt;span&gt;max&lt;/span&gt;&lt;span&gt;(big_int_col), &lt;/span&gt;&lt;span&gt;avg&lt;/span&gt;&lt;span&gt;(small_int_col) &lt;/span&gt;&lt;span&gt;FROM&lt;/span&gt;&lt;span&gt; sbtest1 &lt;/span&gt;&lt;span&gt;WHERE&lt;/span&gt;&lt;span&gt; big_int_col &lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;span&gt; 0&lt;/span&gt;&lt;span&gt; GROUP BY&lt;/span&gt;&lt;span&gt; year_col, set_col &lt;/span&gt;&lt;span&gt;ORDER BY&lt;/span&gt;&lt;span&gt; year_col;&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt;---------------------------------------------------------------------------------------------------------------------+&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;| plan                                                                                                                |&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt;---------------------------------------------------------------------------------------------------------------------+&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;| Project                                                                                                             |&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;|  ├─ columns: [sbtest1.year_col, count(sbtest1.year_col), max(sbtest1.big_int_col), avg(sbtest1.small_int_col)]      |&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;|  └─ Sort(&lt;/span&gt;&lt;span&gt;sbtest1&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;year_col&lt;/span&gt;&lt;span&gt; ASC&lt;/span&gt;&lt;span&gt;)                                                                                      |&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;|      └─ GroupBy                                                                                                     |&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;|          ├─ &lt;/span&gt;&lt;span&gt;select&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;AVG&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;sbtest1&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;small_int_col&lt;/span&gt;&lt;span&gt;), &lt;/span&gt;&lt;span&gt;COUNT&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;sbtest1&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;year_col&lt;/span&gt;&lt;span&gt;), &lt;/span&gt;&lt;span&gt;MAX&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;sbtest1&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;big_int_col&lt;/span&gt;&lt;span&gt;), &lt;/span&gt;&lt;span&gt;sbtest1&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;year_col&lt;/span&gt;&lt;span&gt; |&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;|          ├─ group: &lt;/span&gt;&lt;span&gt;sbtest1&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;year_col&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;sbtest1&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;set_col&lt;/span&gt;&lt;span&gt;                                                                |&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;|          └─ IndexedTableAccess(sbtest1)                                                                             |&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;|              ├─ &lt;/span&gt;&lt;span&gt;index&lt;/span&gt;&lt;span&gt;: [sbtest1.big_int_col]                                                                        |&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;|              ├─ filters: [{(0, ∞)}]                                                                                 |&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;|              └─ columns: [small_int_col big_int_col set_col year_col]                                               |&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt;---------------------------------------------------------------------------------------------------------------------+&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;10&lt;/span&gt;&lt;span&gt; rows&lt;/span&gt;&lt;span&gt; in&lt;/span&gt;&lt;span&gt; set&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;00&lt;/span&gt;&lt;span&gt; sec) &lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Doltgres:&lt;/p&gt;
&lt;pre class="astro-code github-dark" tabindex="0" data-language="sql"&gt;&lt;code&gt;&lt;span class="line"&gt;&lt;span&gt;postgres&lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; explain &lt;/span&gt;&lt;span&gt;SELECT&lt;/span&gt;&lt;span&gt; year_col, &lt;/span&gt;&lt;span&gt;count&lt;/span&gt;&lt;span&gt;(year_col), &lt;/span&gt;&lt;span&gt;max&lt;/span&gt;&lt;span&gt;(big_int_col), &lt;/span&gt;&lt;span&gt;avg&lt;/span&gt;&lt;span&gt;(small_int_col) &lt;/span&gt;&lt;span&gt;FROM&lt;/span&gt;&lt;span&gt; sbtest1 &lt;/span&gt;&lt;span&gt;WHERE&lt;/span&gt;&lt;span&gt; big_int_col &lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt; 0&lt;/span&gt;&lt;span&gt; GROUP BY&lt;/span&gt;&lt;span&gt; year_col, set_col &lt;/span&gt;&lt;span&gt;ORDER BY&lt;/span&gt;&lt;span&gt; year_col;&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;                                                                 plan                                                                  &lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;---------------------------------------------------------------------------------------------------------------------------------------&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt; Project&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;  ├─ columns: [sbtest1.year_col, count(sbtest1.year_col) as count, max(sbtest1.big_int_col) as max, avg(sbtest1.small_int_col) as avg]&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;  └─ Sort(&lt;/span&gt;&lt;span&gt;sbtest1&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;year_col&lt;/span&gt;&lt;span&gt; ASC&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;      └─ GroupBy&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;          ├─ &lt;/span&gt;&lt;span&gt;select&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;COUNT&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;sbtest1&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;year_col&lt;/span&gt;&lt;span&gt;), &lt;/span&gt;&lt;span&gt;MAX&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;sbtest1&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;big_int_col&lt;/span&gt;&lt;span&gt;), &lt;/span&gt;&lt;span&gt;avg&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;sbtest1&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;small_int_col&lt;/span&gt;&lt;span&gt;), &lt;/span&gt;&lt;span&gt;sbtest1&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;year_col&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;          ├─ group: &lt;/span&gt;&lt;span&gt;sbtest1&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;year_col&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;sbtest1&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;set_col&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;          └─ IndexedTableAccess(sbtest1)&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;              ├─ &lt;/span&gt;&lt;span&gt;index&lt;/span&gt;&lt;span&gt;: [sbtest1.big_int_col]&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;              ├─ filters: [{(NULL, 0)}]&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;              └─ columns: [small_int_col big_int_col set_col year_col]&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;MySQL:&lt;/p&gt;
&lt;pre class="astro-code github-dark" tabindex="0" data-language="sql"&gt;&lt;code&gt;&lt;span class="line"&gt;&lt;span&gt;mysql&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;span&gt; explain analyze &lt;/span&gt;&lt;span&gt;SELECT&lt;/span&gt;&lt;span&gt; year_col, &lt;/span&gt;&lt;span&gt;count&lt;/span&gt;&lt;span&gt;(year_col), &lt;/span&gt;&lt;span&gt;max&lt;/span&gt;&lt;span&gt;(big_int_col), &lt;/span&gt;&lt;span&gt;avg&lt;/span&gt;&lt;span&gt;(small_int_col) &lt;/span&gt;&lt;span&gt;FROM&lt;/span&gt;&lt;span&gt; sbtest1 &lt;/span&gt;&lt;span&gt;WHERE&lt;/span&gt;&lt;span&gt; big_int_col &lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;span&gt; 0&lt;/span&gt;&lt;span&gt; GROUP BY&lt;/span&gt;&lt;span&gt; year_col, set_col &lt;/span&gt;&lt;span&gt;ORDER BY&lt;/span&gt;&lt;span&gt; year_col;&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt;----------------------------------------------------------------------------------------------------------------------------------------------------------------------------+&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;| EXPLAIN                                                                                                                                                                    |&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt;----------------------------------------------------------------------------------------------------------------------------------------------------------------------------+&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;| &lt;/span&gt;&lt;span&gt;-&gt;&lt;/span&gt;&lt;span&gt; Sort: &lt;/span&gt;&lt;span&gt;sbtest1&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;year_col&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;sbtest1&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;set_col&lt;/span&gt;&lt;span&gt;  (actual &lt;/span&gt;&lt;span&gt;time=&lt;/span&gt;&lt;span&gt;16&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;4&lt;/span&gt;&lt;span&gt;..&lt;/span&gt;&lt;span&gt;16&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;4&lt;/span&gt;&lt;span&gt; rows=&lt;/span&gt;&lt;span&gt;765&lt;/span&gt;&lt;span&gt; loops&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;    -&gt;&lt;/span&gt;&lt;span&gt; Table&lt;/span&gt;&lt;span&gt; scan &lt;/span&gt;&lt;span&gt;on&lt;/span&gt;&lt;span&gt; &amp;#x3C;&lt;/span&gt;&lt;span&gt;temporary&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;span&gt;  (actual &lt;/span&gt;&lt;span&gt;time=&lt;/span&gt;&lt;span&gt;15&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;9&lt;/span&gt;&lt;span&gt;..&lt;/span&gt;&lt;span&gt;16&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt; rows=&lt;/span&gt;&lt;span&gt;765&lt;/span&gt;&lt;span&gt; loops&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;        -&gt;&lt;/span&gt;&lt;span&gt; Aggregate&lt;/span&gt;&lt;span&gt; using&lt;/span&gt;&lt;span&gt; temporary &lt;/span&gt;&lt;span&gt;table&lt;/span&gt;&lt;span&gt;  (actual &lt;/span&gt;&lt;span&gt;time=&lt;/span&gt;&lt;span&gt;15&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;9&lt;/span&gt;&lt;span&gt;..&lt;/span&gt;&lt;span&gt;15&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;9&lt;/span&gt;&lt;span&gt; rows=&lt;/span&gt;&lt;span&gt;765&lt;/span&gt;&lt;span&gt; loops&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;            -&gt;&lt;/span&gt;&lt;span&gt; Filter&lt;/span&gt;&lt;span&gt;: (&lt;/span&gt;&lt;span&gt;sbtest1&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;big_int_col&lt;/span&gt;&lt;span&gt; &gt;&lt;/span&gt;&lt;span&gt; 0&lt;/span&gt;&lt;span&gt;)  (cost&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;995&lt;/span&gt;&lt;span&gt; rows=&lt;/span&gt;&lt;span&gt;4896&lt;/span&gt;&lt;span&gt;) (actual &lt;/span&gt;&lt;span&gt;time=&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;222&lt;/span&gt;&lt;span&gt;..&lt;/span&gt;&lt;span&gt;7&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;37&lt;/span&gt;&lt;span&gt; rows=&lt;/span&gt;&lt;span&gt;4896&lt;/span&gt;&lt;span&gt; loops&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;                -&gt;&lt;/span&gt;&lt;span&gt; Table&lt;/span&gt;&lt;span&gt; scan &lt;/span&gt;&lt;span&gt;on&lt;/span&gt;&lt;span&gt; sbtest1  (cost&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;995&lt;/span&gt;&lt;span&gt; rows=&lt;/span&gt;&lt;span&gt;9707&lt;/span&gt;&lt;span&gt;) (actual &lt;/span&gt;&lt;span&gt;time=&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;219&lt;/span&gt;&lt;span&gt;..&lt;/span&gt;&lt;span&gt;6&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;36&lt;/span&gt;&lt;span&gt; rows=&lt;/span&gt;&lt;span&gt;10000&lt;/span&gt;&lt;span&gt; loops&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt; |&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt;----------------------------------------------------------------------------------------------------------------------------------------------------------------------------+&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt; row&lt;/span&gt;&lt;span&gt; in&lt;/span&gt;&lt;span&gt; set&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;02&lt;/span&gt;&lt;span&gt; sec)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Postgres:&lt;/p&gt;
&lt;pre class="astro-code github-dark" tabindex="0" data-language="sql"&gt;&lt;code&gt;&lt;span class="line"&gt;&lt;span&gt;postgres&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;# explain analyze &lt;/span&gt;&lt;span&gt;SELECT&lt;/span&gt;&lt;span&gt; year_col, &lt;/span&gt;&lt;span&gt;count&lt;/span&gt;&lt;span&gt;(year_col), &lt;/span&gt;&lt;span&gt;max&lt;/span&gt;&lt;span&gt;(big_int_col), &lt;/span&gt;&lt;span&gt;avg&lt;/span&gt;&lt;span&gt;(small_int_col) &lt;/span&gt;&lt;span&gt;FROM&lt;/span&gt;&lt;span&gt; sbtest1 &lt;/span&gt;&lt;span&gt;WHERE&lt;/span&gt;&lt;span&gt; big_int_col &lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt; 0&lt;/span&gt;&lt;span&gt; GROUP BY&lt;/span&gt;&lt;span&gt; year_col, set_col &lt;/span&gt;&lt;span&gt;ORDER BY&lt;/span&gt;&lt;span&gt; year_col;&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;                                                      QUERY PLAN                                                      &lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;----------------------------------------------------------------------------------------------------------------------&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt; Sort  (cost&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;427&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;98&lt;/span&gt;&lt;span&gt;..&lt;/span&gt;&lt;span&gt;429&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;89&lt;/span&gt;&lt;span&gt; rows=&lt;/span&gt;&lt;span&gt;765&lt;/span&gt;&lt;span&gt; width&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;55&lt;/span&gt;&lt;span&gt;) (actual &lt;/span&gt;&lt;span&gt;time=&lt;/span&gt;&lt;span&gt;6&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;003&lt;/span&gt;&lt;span&gt;..&lt;/span&gt;&lt;span&gt;6&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;057&lt;/span&gt;&lt;span&gt; rows=&lt;/span&gt;&lt;span&gt;765&lt;/span&gt;&lt;span&gt; loops&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;   Sort &lt;/span&gt;&lt;span&gt;Key&lt;/span&gt;&lt;span&gt;: year_col&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;   Sort Method: quicksort  Memory: 84kB&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;   -&gt;&lt;/span&gt;&lt;span&gt;  HashAggregate  (cost&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;381&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;77&lt;/span&gt;&lt;span&gt;..&lt;/span&gt;&lt;span&gt;391&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;34&lt;/span&gt;&lt;span&gt; rows=&lt;/span&gt;&lt;span&gt;765&lt;/span&gt;&lt;span&gt; width&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;55&lt;/span&gt;&lt;span&gt;) (actual &lt;/span&gt;&lt;span&gt;time=&lt;/span&gt;&lt;span&gt;5&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;183&lt;/span&gt;&lt;span&gt;..&lt;/span&gt;&lt;span&gt;5&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;672&lt;/span&gt;&lt;span&gt; rows=&lt;/span&gt;&lt;span&gt;765&lt;/span&gt;&lt;span&gt; loops&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;         Group &lt;/span&gt;&lt;span&gt;Key&lt;/span&gt;&lt;span&gt;: year_col, set_col&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;         Batches&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;  Memory Usage: 297kB&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;         -&gt;&lt;/span&gt;&lt;span&gt;  Seq Scan &lt;/span&gt;&lt;span&gt;on&lt;/span&gt;&lt;span&gt; sbtest1  (cost&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;00&lt;/span&gt;&lt;span&gt;..&lt;/span&gt;&lt;span&gt;318&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;00&lt;/span&gt;&lt;span&gt; rows=&lt;/span&gt;&lt;span&gt;5102&lt;/span&gt;&lt;span&gt; width&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;17&lt;/span&gt;&lt;span&gt;) (actual &lt;/span&gt;&lt;span&gt;time=&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;005&lt;/span&gt;&lt;span&gt;..&lt;/span&gt;&lt;span&gt;2&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;315&lt;/span&gt;&lt;span&gt; rows=&lt;/span&gt;&lt;span&gt;5104&lt;/span&gt;&lt;span&gt; loops&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;               Filter&lt;/span&gt;&lt;span&gt;: (big_int_col &lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt; 0&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;               Rows&lt;/span&gt;&lt;span&gt; Removed &lt;/span&gt;&lt;span&gt;by&lt;/span&gt;&lt;span&gt; Filter&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;4896&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt; Planning &lt;/span&gt;&lt;span&gt;Time&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;711&lt;/span&gt;&lt;span&gt; ms&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt; Execution &lt;/span&gt;&lt;span&gt;Time&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;6&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;327&lt;/span&gt;&lt;span&gt; ms&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;11&lt;/span&gt;&lt;span&gt; rows&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Interesting, Dolt and Doltgres are using the secondary index defined over &lt;code&gt;big_int_col&lt;/code&gt;, while MySQL and Postgres just perform a full table scan.
Since the values in &lt;code&gt;big_int_col&lt;/code&gt; are uniformly distributed around 0, the filter &lt;code&gt;where big_int_col &gt; 0&lt;/code&gt; excludes roughly half the columns.
It appears the MySQL and Postgres analyzer is smart enough to recognize that the additional lookup is suboptimal.&lt;/p&gt;
&lt;p&gt;The flame graph supports this conclusion.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.dolthub.com/blogimages/secondary_key_flame_graph.png/92035b7c96f6eca9e5d49c2d5d5da1fc2b4059f0aa45196f3bd4c46d8d0cc18e.webp" alt="flame graph"&gt;&lt;/p&gt;
&lt;p&gt;Here, we see that a large portion of the CPU is spent in &lt;code&gt;prolly.Map.Get&lt;/code&gt;, which is the secondary key lookup.&lt;/p&gt;
&lt;h1 id="optimization"&gt;Optimization&lt;a class="anchor-link" aria-label="Link to heading" href="#optimization"&gt;#&lt;/a&gt;&lt;/h1&gt;
&lt;p&gt;Currently, our analyzer will always pick an index when applicable because we assume that will always be better.
Evidently, we have discovered that isn’t always the case.
We need to modify the existing coster to consider full table scans depending on how well the index filters the results.
Fortunately, we implemented statistics a while ago, and we can use the histograms there to get a good estimate of how selective the filter is.&lt;/p&gt;
&lt;p&gt;We added these new heuristics to the coster:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A primary key is always better than no index&lt;/li&gt;
&lt;li&gt;A secondary index should only be used over a full table scan if it selects fewer than 25% of rows&lt;/li&gt;
&lt;li&gt;A covering index is always better than no index&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The rest of the coster remains the same.
In the future, we should take into consideration things like the complexity of the filter, if the table can fit into memory, size of output row, etc., but this is good enough for now.
If you’d like to read the implementation in greater detail, you can check out these PRs:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/dolthub/go-mysql-server/pull/3639"&gt;dolthub/go-mysql-server#3659&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/dolthub/go-mysql-server/pull/3657"&gt;dolthub/go-mysql-server#3657&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;However, it took some extra work to carry these performance benefits over to Doltgres.
While Doltgres does use the same costing logic, statistics weren’t even enabled.
After enabling statistics, fixing some bugs, and adding some logic to get histograms working, these are the results:&lt;/p&gt;










































































































































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;benchmark&lt;/th&gt;&lt;th&gt;dolt&lt;/th&gt;&lt;th&gt;doltgres&lt;/th&gt;&lt;th&gt;mysql&lt;/th&gt;&lt;th&gt;postgres&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;covering_index_scan&lt;/td&gt;&lt;td&gt;2.35&lt;/td&gt;&lt;td&gt;2.43&lt;/td&gt;&lt;td&gt;17.01&lt;/td&gt;&lt;td&gt;17.95&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;groupby_scan&lt;/td&gt;&lt;td&gt;62.19&lt;/td&gt;&lt;td&gt;82.96&lt;/td&gt;&lt;td&gt;144.97&lt;/td&gt;&lt;td&gt;40.37&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;index_join&lt;/td&gt;&lt;td&gt;1.93&lt;/td&gt;&lt;td&gt;2.30&lt;/td&gt;&lt;td&gt;3.43&lt;/td&gt;&lt;td&gt;1.82&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;index_join_scan&lt;/td&gt;&lt;td&gt;1.32&lt;/td&gt;&lt;td&gt;1.67&lt;/td&gt;&lt;td&gt;4.18&lt;/td&gt;&lt;td&gt;0.67&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;index_scan&lt;/td&gt;&lt;td&gt;204.11&lt;/td&gt;&lt;td&gt;484.44&lt;/td&gt;&lt;td&gt;350.33&lt;/td&gt;&lt;td&gt;179.94&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;oltp_point_select&lt;/td&gt;&lt;td&gt;0.25&lt;/td&gt;&lt;td&gt;0.40&lt;/td&gt;&lt;td&gt;0.19&lt;/td&gt;&lt;td&gt;0.15&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;oltp_read_only&lt;/td&gt;&lt;td&gt;4.91&lt;/td&gt;&lt;td&gt;6.55&lt;/td&gt;&lt;td&gt;3.68&lt;/td&gt;&lt;td&gt;2.66&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;select_random_points&lt;/td&gt;&lt;td&gt;0.52&lt;/td&gt;&lt;td&gt;0.74&lt;/td&gt;&lt;td&gt;0.36&lt;/td&gt;&lt;td&gt;0.22&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;select_random_ranges&lt;/td&gt;&lt;td&gt;0.65&lt;/td&gt;&lt;td&gt;1.04&lt;/td&gt;&lt;td&gt;0.39&lt;/td&gt;&lt;td&gt;0.42&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;table_scan&lt;/td&gt;&lt;td&gt;204.47&lt;/td&gt;&lt;td&gt;475.79&lt;/td&gt;&lt;td&gt;350.33&lt;/td&gt;&lt;td&gt;179.94&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;types_table_scan&lt;/td&gt;&lt;td&gt;458.96&lt;/td&gt;&lt;td&gt;1213.57&lt;/td&gt;&lt;td&gt;759.88&lt;/td&gt;&lt;td&gt;427.07&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;oltp_delete_insert&lt;/td&gt;&lt;td&gt;6.21&lt;/td&gt;&lt;td&gt;6.79&lt;/td&gt;&lt;td&gt;7.70&lt;/td&gt;&lt;td&gt;2.22&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;oltp_insert&lt;/td&gt;&lt;td&gt;3.19&lt;/td&gt;&lt;td&gt;3.89&lt;/td&gt;&lt;td&gt;4.10&lt;/td&gt;&lt;td&gt;1.10&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;oltp_read_write&lt;/td&gt;&lt;td&gt;11.24&lt;/td&gt;&lt;td&gt;14.21&lt;/td&gt;&lt;td&gt;8.90&lt;/td&gt;&lt;td&gt;4.33&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;oltp_update_index&lt;/td&gt;&lt;td&gt;3.30&lt;/td&gt;&lt;td&gt;3.82&lt;/td&gt;&lt;td&gt;4.41&lt;/td&gt;&lt;td&gt;1.14&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;oltp_update_non_index&lt;/td&gt;&lt;td&gt;3.02&lt;/td&gt;&lt;td&gt;3.49&lt;/td&gt;&lt;td&gt;4.18&lt;/td&gt;&lt;td&gt;1.12&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;oltp_write_only&lt;/td&gt;&lt;td&gt;6.32&lt;/td&gt;&lt;td&gt;7.30&lt;/td&gt;&lt;td&gt;5.18&lt;/td&gt;&lt;td&gt;1.79&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;types_delete_insert&lt;/td&gt;&lt;td&gt;6.79&lt;/td&gt;&lt;td&gt;7.43&lt;/td&gt;&lt;td&gt;8.43&lt;/td&gt;&lt;td&gt;2.30&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;On Dolt, this brought down the latency for &lt;code&gt;groupby_scan&lt;/code&gt; from &lt;code&gt;144.96ms&lt;/code&gt; to &lt;code&gt;62.19ms&lt;/code&gt;; this is a &lt;code&gt;57.1%&lt;/code&gt; improvement!
On Doltgres, &lt;code&gt;groupby_scan&lt;/code&gt; latency decreased from &lt;code&gt;147.61ms&lt;/code&gt; to &lt;code&gt;82.96ms&lt;/code&gt;, which is a &lt;code&gt;43.80%&lt;/code&gt; improvement.
With these optimizations, Dolt’s latency for &lt;code&gt;groupby_scan&lt;/code&gt; is less than half of MySQL’s.
Unfortunately, Postgres still pulls way ahead with their latency being less than half that of Doltgres’s.&lt;/p&gt;
&lt;p&gt;While we were focused on &lt;code&gt;groupby_scan&lt;/code&gt;, there were also some improvements to &lt;code&gt;index_scan&lt;/code&gt;.
Dolt went from &lt;code&gt;219.36ms&lt;/code&gt; down to &lt;code&gt;204.11ms&lt;/code&gt;, which is a &lt;code&gt;6.95%&lt;/code&gt; improvement.
Doltgres went from &lt;code&gt;493.24ms&lt;/code&gt; down to &lt;code&gt;484.44ms&lt;/code&gt;, which is a &lt;code&gt;1.78%&lt;/code&gt; improvement.
Interestingly, a byproduct of this optimization is that the &lt;code&gt;index_scan&lt;/code&gt; and &lt;code&gt;table_scan&lt;/code&gt; benchmarks are essentially the same now.
The resulting plans from both these queries avoid the secondary index, making them both full table scans with a filter.&lt;/p&gt;
&lt;h1 id="conclusion"&gt;Conclusion&lt;a class="anchor-link" aria-label="Link to heading" href="#conclusion"&gt;#&lt;/a&gt;&lt;/h1&gt;
&lt;p&gt;We continue to bring performance improvements to both Dolt and Doltgres.
Making our coster just a little bit smarter has resulted in our &lt;code&gt;groupby_scan&lt;/code&gt; benchmarks running two times faster.
Stay tuned to hear more about the performance improvements included in the upcoming Doltgres 1.0 release!
Feel free to chat with us on &lt;a href="https://discord.gg/gqr7K4VNKe"&gt;Discord&lt;/a&gt; or file a &lt;a href="https://github.com/dolthub/dolt/issues"&gt;Github issue&lt;/a&gt;.&lt;/p&gt;</content:encoded>
      <dc:creator>James Cor</dc:creator>
      <category>technical</category>
      <category>performance</category>
    </item>
    <item>
      <title>Running Ito, a Runtime Analysis Code Review Tool, on Doltgres</title>
      <link>https://dolthub.com/blog/2026-07-20-ito-ai-qa-for-doltgresql/</link>
      <guid isPermaLink="true">https://dolthub.com/blog/2026-07-20-ito-ai-qa-for-doltgresql/</guid>
      <description>For the past six weeks, we've been running Ito, a runtime analysis code review tool that works off of GitHub PRs and builds and runs your application instead of just reading the diff. We've had success using this on our Doltgresql repo and in this blog post we'll show what it's caught, how it works, and where it's let us down.</description>
      <pubDate>Mon, 20 Jul 2026 00:00:00 GMT</pubDate>
      <content:encoded>&lt;p&gt;We ship a lot of code at DoltHub, and coding agents, like Claude and Cursor, help us produce code even faster. However, we still need a human to review all of that code and sign off on it before it goes into our products. About six weeks ago, we started using &lt;a href="https://www.ito.ai/"&gt;Ito&lt;/a&gt; on the repository for our Postgres-compatible version-controlled database, &lt;a href="https://github.com/dolthub/doltgresql"&gt;doltgresql&lt;/a&gt;. Ito is &lt;del&gt;an AI QA agent&lt;/del&gt; a runtime analysis code review tool that works off of GitHub PRs and takes a different approach than other AI tools that analyze PRs: instead of just reading your PR diff and commenting on it, Ito actually builds your application from the PR branch, runs it in a real environment, and exercises it like a user would. On Doltgres that means standing up a real &lt;code&gt;doltgres&lt;/code&gt; server and running SQL queries against it. Ito can handle other types of applications, too, for example, running your web application and capturing a screen recording of the UI testing. This post is our review of Ito. We’ll cover what Ito is, how to enable it, dig into a couple of concrete examples of bugs it’s helped us find, and close out with the numbers on how effective it’s actually been over the six weeks we’ve been running it, including our estimated signal-to-noise ratio on the findings it’s raised. Ito doesn’t replace a human reviewer, but it augments our review process and has been successful at helping us find bugs and broken edge cases.&lt;/p&gt;
&lt;h1 id="what-is-ito"&gt;What is Ito?&lt;a class="anchor-link" aria-label="Link to heading" href="#what-is-ito"&gt;#&lt;/a&gt;&lt;/h1&gt;
&lt;p&gt;AI tools that analyze GitHub PRs tend to work off the diff of your changes: they feed the changed lines (and &lt;em&gt;maybe&lt;/em&gt; some surrounding context) to an LLM and ask it to spot problems. That’s useful for catching sloppy code and some obvious bugs, but it can’t tell you how your product actually runs with these changes, and it can’t test if your product is behaving as expected. A good code reviewer understands the codebase and the product experience and reviews the changes in that larger context.&lt;/p&gt;
&lt;p&gt;Ito is different. Ito builds a containerized version of your application from the PR branch and actually runs it. It reads the diff and the PR description to figure out what area of your app is affected, generates a set of test scenarios targeting that area, and then executes them against the running application. For a web app, that means an agent driving a real browser session against your frontend and backend together. For Doltgres, it means opening a Postgres connection to a running &lt;code&gt;doltgres&lt;/code&gt; server and issuing SQL statements. Either way, the approach is the same: it’s checking how your code actually behaves, not just what the diff looks like.&lt;/p&gt;
&lt;p&gt;Every PR gets a comment from Ito summarizing what it ran, what passed, what failed, and a severity rating for anything it flagged, plus a top-line verdict on whether Ito thinks the PR is safe to merge. Failures come with reproduction instructions that make it easy to repro the failure yourself. For web-based applications, you even get a screen recording you can view that shows you exactly what Ito tested and how your application responded. The Ito website provides a more detailed view of the testing Ito performed. &lt;a href="https://app.ito.ai/share/61ad6bd4-2df1-4110-a2e8-e02915a5f39a?tab=details&amp;#x26;_gl=1*7w1ugk*_gcl_au*MTAzNjIwNDM2OS4xNzgxNTY1Mzgw*_ga*MTgwMTA2NzQyNC4xNzg0MTQ4MzUx*_ga_BRWMBEWN9V*czE3ODQxNDgzNTEkbzEkZzEkdDE3ODQxNDkxNzgkajU2JGwwJGgw"&gt;Here’s an example of a detailed report from the Ito website&lt;/a&gt; for changes it tested against the open-source chatwoot project.&lt;/p&gt;
&lt;p&gt;Below, you can see an example of what an Ito summary comment looks like on one of our PRs. It shows us that it ran 15 tests and two of them failed. It gives a summary of the findings and a recommendation that this PR is not safe to merge yet.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.dolthub.com/blogimages/ito-summary-comment.png/836b6e236713080eccaa00ad90c3de15d8ba44a12906917cf19c5ebd8b9ff522.webp" alt="Ito summary comment"&gt;&lt;/p&gt;
&lt;p&gt;In that same comment, you can drill into the “Tests run by Ito” section to see more detail. We’ll revisit these same findings below, when we talk about what kinds of bugs Ito has found for us.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.dolthub.com/blogimages/ito-summary-comment-tests-run.png/88550959d66c128a3d70a797c445bd69b8b08fabceb9e238cdea1697db9dd4f0.webp" alt="Ito summary comment tests run"&gt;&lt;/p&gt;
&lt;h1 id="turning-ito-on"&gt;Turning Ito On&lt;a class="anchor-link" aria-label="Link to heading" href="#turning-ito-on"&gt;#&lt;/a&gt;&lt;/h1&gt;
&lt;p&gt;Setting up Ito is very easy. You create an account at &lt;a href="https://app.ito.ai"&gt;app.ito.ai&lt;/a&gt;, install the &lt;a href="https://github.com/apps/itoqa"&gt;Ito GitHub App&lt;/a&gt; on your organization (or a personal account), and select which repositories you want it watching. There’s no YAML to write and no config to tune before your first run. Ito maps out your codebase from what’s already there, and from that point on, it automatically picks up new and updated pull requests on the repos you selected. As PRs are reviewed, a bot account, &lt;code&gt;itoqa&lt;/code&gt;, comments with results as they come in.&lt;/p&gt;
&lt;p&gt;You can add custom variables, seed data, or secrets later if you want to point it at specific fixtures, but we haven’t needed to yet. It’s been running against our repo with the defaults.&lt;/p&gt;
&lt;p&gt;Ito provides &lt;a href="https://www.ito.ai/pricing"&gt;a free trial&lt;/a&gt; for startups and small teams, which makes it easy to test out Ito with your app and see how it can help your team.&lt;/p&gt;
&lt;h1 id="ito-in-action"&gt;Ito in Action&lt;a class="anchor-link" aria-label="Link to heading" href="#ito-in-action"&gt;#&lt;/a&gt;&lt;/h1&gt;
&lt;p&gt;Let’s take a closer look at some real examples of how Ito has helped us catch some pretty tricky bugs. One of the things we’ve noticed is that Ito does a really good job of testing edge cases in SQL behavior. SQL is a well-documented spec, so it makes sense that a generative AI tool like Ito could use that information to effectively test edge cases and find bugs. This is where we’ve seen Ito be most helpful so far in our experience testing our product.&lt;/p&gt;
&lt;p&gt;Two particularly good examples come out of &lt;a href="https://github.com/dolthub/doltgresql/pull/2913"&gt;PR #2913&lt;/a&gt;, which added native &lt;a href="https://www.postgresql.org/docs/current/tutorial-window.html"&gt;window function support&lt;/a&gt; to DoltgreSQL so that we could match PostgreSQL behavior exactly. Previously, Doltgres was relying on go-mysql-server’s implementation, which is implemented to match MySQL behavior, and doesn’t always match PostgreSQL’s behavior, particularly for return types. When I thought I was done with the work, I looked at the PR and noticed that Ito had added comments for two interesting issues it had found.&lt;/p&gt;
&lt;h2 id="bug-named-window-reference-over-w-produces-a-full-partition-sum-instead-of-a-running-sum-within-each-partition"&gt;Bug: Named window reference &lt;code&gt;OVER w&lt;/code&gt; produces a full-partition SUM instead of a running SUM within each partition&lt;a class="anchor-link" aria-label="Link to heading" href="#bug-named-window-reference-over-w-produces-a-full-partition-sum-instead-of-a-running-sum-within-each-partition"&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;SQL window function syntax allows you to define a “window” of result rows over which aggregate functions will operate. These window definitions can be inline (e.g. &lt;code&gt;SELECT sum(y) OVER (ORDER BY z)&lt;/code&gt;) or they be defined as a named window (e.g. &lt;code&gt;SELECT sum(y) over (w1) FROM a WINDOW w1 AS (order by z);&lt;/code&gt;). In either syntax form, if the &lt;code&gt;ORDER BY&lt;/code&gt; clause is not specified for a window, then the window function operates over the full partition. In other words, there is a single partition containing all rows that the window function operates on. However, there was a bug hiding deep in our database engine where the correct window framing was applied only if the &lt;code&gt;ORDER BY&lt;/code&gt; clause was defined inline. If the &lt;code&gt;ORDER BY&lt;/code&gt; clause was defined in a named window, our code saw that there wasn’t an &lt;code&gt;ORDER BY&lt;/code&gt; clause specified inline, and incorrectly defaulted to framing the window over the full result set. To execute this query correctly, our engine needed to recognize that a named window was being used and examine it to see if it declared an &lt;code&gt;ORDER BY&lt;/code&gt; clause. Only when no inline window definition &lt;strong&gt;and&lt;/strong&gt; no named window definition contained an &lt;code&gt;ORDER BY&lt;/code&gt; clause should the engine default the window framing to a single window over the entire result set.&lt;/p&gt;
&lt;p&gt;To make this more concrete, check out these statements that repro the issue:&lt;/p&gt;
&lt;pre class="astro-code github-dark" tabindex="0" data-language="sql"&gt;&lt;code&gt;&lt;span class="line"&gt;&lt;span&gt;CREATE&lt;/span&gt;&lt;span&gt; TABLE&lt;/span&gt;&lt;span&gt; a&lt;/span&gt;&lt;span&gt; (x &lt;/span&gt;&lt;span&gt;INT&lt;/span&gt;&lt;span&gt; PRIMARY KEY&lt;/span&gt;&lt;span&gt;, y &lt;/span&gt;&lt;span&gt;INT&lt;/span&gt;&lt;span&gt;, z &lt;/span&gt;&lt;span&gt;INT&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;INSERT INTO&lt;/span&gt;&lt;span&gt; a &lt;/span&gt;&lt;span&gt;VALUES&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;),(&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;),(&lt;/span&gt;&lt;span&gt;2&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;2&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;),(&lt;/span&gt;&lt;span&gt;3&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;),(&lt;/span&gt;&lt;span&gt;4&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;),(&lt;/span&gt;&lt;span&gt;5&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;3&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;-- the inline version worked correctly&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;SELECT&lt;/span&gt;&lt;span&gt; sum&lt;/span&gt;&lt;span&gt;(y) &lt;/span&gt;&lt;span&gt;over&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;ORDER BY&lt;/span&gt;&lt;span&gt; z) &lt;/span&gt;&lt;span&gt;FROM&lt;/span&gt;&lt;span&gt; a &lt;/span&gt;&lt;span&gt;ORDER BY&lt;/span&gt;&lt;span&gt; x;&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;-- correct:         0, 1, 3, 3, 4, 7   (running/cumulative sum)&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;-- referencing a named window did NOT work correctly&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;SELECT&lt;/span&gt;&lt;span&gt; sum&lt;/span&gt;&lt;span&gt;(y) &lt;/span&gt;&lt;span&gt;over&lt;/span&gt;&lt;span&gt; (w1) &lt;/span&gt;&lt;span&gt;FROM&lt;/span&gt;&lt;span&gt; a &lt;/span&gt;&lt;span&gt;WINDOW&lt;/span&gt;&lt;span&gt; w1 &lt;/span&gt;&lt;span&gt;AS&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;order by&lt;/span&gt;&lt;span&gt; z) &lt;/span&gt;&lt;span&gt;ORDER BY&lt;/span&gt;&lt;span&gt; x;&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;-- buggy (pre-fix): 7, 7, 7, 7, 7, 7   (full-partition sum on every row)&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;-- correct:         0, 1, 3, 3, 4, 7   (running/cumulative sum)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Note that when the window definition was defined inline (i.e. &lt;code&gt;SELECT sum(y) over (order by z) FROM a ORDER BY x;&lt;/code&gt;), this query produced the correct results. This bug was specific to referencing a named window definition.&lt;/p&gt;
&lt;p&gt;Here’s the comment Ito added for this issue:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.dolthub.com/blogimages/ito-issue-comment-named-window-reference-via-over.png/f622fcebebdf274072f2d9028a4cc096bb41bcfb8d229936eae7a2cda85566c9.webp" alt="Ito issue comment named window reference via OVER"&gt;&lt;/p&gt;
&lt;p&gt;Inside that comment, there is a lot of detail, including a summary of the finding, evidence for repro’ing the problem, and even a sample prompt you can pass to a coding agent to start debugging the issue. I was particularly interested in the repro instructions. I find that’s usually the fastest way for me to understand an issue. When testing a UI application, Ito will actually provide evidence as a screen recording of the testing with your UI, so you can see exactly how it was triggered and how your app responded. Since DoltgreSQL is a server process and not a UI, the evidence Ito provided for us is a repro script and some analysis. You can see the full evidence Ito provided below.&lt;/p&gt;
&lt;details&gt;                                                                                                                                                  
&lt;summary&gt;Click to see Ito's evidence for this bug&lt;/summary&gt;
&lt;h3 id="setup-context"&gt;setup context&lt;a class="anchor-link" aria-label="Link to heading" href="#setup-context"&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;h1 id="setup_keys-default-superuser"&gt;setup_keys: default-superuser&lt;a class="anchor-link" aria-label="Link to heading" href="#setup_keys-default-superuser"&gt;#&lt;/a&gt;&lt;/h1&gt;
&lt;h1 id="timestamp-2026-07-13t201156578z-test-execution-for-named-window-over-w"&gt;timestamp: 2026-07-13T20:11:56.578Z test execution for named window OVER w&lt;a class="anchor-link" aria-label="Link to heading" href="#timestamp-2026-07-13t201156578z-test-execution-for-named-window-over-w"&gt;#&lt;/a&gt;&lt;/h1&gt;
&lt;h3 id="reproduction-script"&gt;reproduction script&lt;a class="anchor-link" aria-label="Link to heading" href="#reproduction-script"&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;pre class="astro-code github-dark" tabindex="0" data-language="sql"&gt;&lt;code&gt;&lt;span class="line"&gt;&lt;span&gt;DROP&lt;/span&gt;&lt;span&gt; TABLE&lt;/span&gt;&lt;span&gt; IF&lt;/span&gt;&lt;span&gt; EXISTS&lt;/span&gt;&lt;span&gt; t_named;&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;CREATE&lt;/span&gt;&lt;span&gt; TABLE&lt;/span&gt;&lt;span&gt; t_named&lt;/span&gt;&lt;span&gt;(id &lt;/span&gt;&lt;span&gt;int&lt;/span&gt;&lt;span&gt;, grp &lt;/span&gt;&lt;span&gt;int&lt;/span&gt;&lt;span&gt;, amt &lt;/span&gt;&lt;span&gt;int&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;INSERT INTO&lt;/span&gt;&lt;span&gt; t_named &lt;/span&gt;&lt;span&gt;VALUES&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;10&lt;/span&gt;&lt;span&gt;),(&lt;/span&gt;&lt;span&gt;2&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;20&lt;/span&gt;&lt;span&gt;),(&lt;/span&gt;&lt;span&gt;3&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;2&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;5&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;SELECT&lt;/span&gt;&lt;span&gt; id, &lt;/span&gt;&lt;span&gt;SUM&lt;/span&gt;&lt;span&gt;(amt) &lt;/span&gt;&lt;span&gt;OVER&lt;/span&gt;&lt;span&gt; w &lt;/span&gt;&lt;span&gt;AS&lt;/span&gt;&lt;span&gt; s&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;FROM&lt;/span&gt;&lt;span&gt; t_named&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;WINDOW&lt;/span&gt;&lt;span&gt; w &lt;/span&gt;&lt;span&gt;AS&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;PARTITION&lt;/span&gt;&lt;span&gt; BY&lt;/span&gt;&lt;span&gt; grp &lt;/span&gt;&lt;span&gt;ORDER BY&lt;/span&gt;&lt;span&gt; id)&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;ORDER BY&lt;/span&gt;&lt;span&gt; id;&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;SELECT&lt;/span&gt;&lt;span&gt; id, &lt;/span&gt;&lt;span&gt;SUM&lt;/span&gt;&lt;span&gt;(amt) &lt;/span&gt;&lt;span&gt;OVER&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;PARTITION&lt;/span&gt;&lt;span&gt; BY&lt;/span&gt;&lt;span&gt; grp &lt;/span&gt;&lt;span&gt;ORDER BY&lt;/span&gt;&lt;span&gt; id) &lt;/span&gt;&lt;span&gt;AS&lt;/span&gt;&lt;span&gt; s&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;FROM&lt;/span&gt;&lt;span&gt; t_named&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;ORDER BY&lt;/span&gt;&lt;span&gt; id;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id="observed-output"&gt;observed output&lt;a class="anchor-link" aria-label="Link to heading" href="#observed-output"&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;DROP TABLE
CREATE TABLE
INSERT 0 3
id | s
----+----
1 | 30
2 | 30
3 | 5
(3 rows)&lt;/p&gt;
&lt;h3 id="inline-baseline-output"&gt;inline baseline output&lt;a class="anchor-link" aria-label="Link to heading" href="#inline-baseline-output"&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;id | s
----+----
1 | 10
2 | 30
3 | 5
(3 rows)&lt;/p&gt;
&lt;h3 id="tabreadback-evidence"&gt;tab/readback evidence&lt;a class="anchor-link" aria-label="Link to heading" href="#tabreadback-evidence"&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;h1 id="test_start-executed-named-window-over-w-query-against-t_named"&gt;test_start: Executed named window OVER w query against t_named&lt;a class="anchor-link" aria-label="Link to heading" href="#test_start-executed-named-window-over-w-query-against-t_named"&gt;#&lt;/a&gt;&lt;/h1&gt;
&lt;h1 id="test_start-result-returned-id1-s30-id2-s30-id3-s5---partition-by-grp-works-but-order-by-id-is-ignored"&gt;test_start result: Returned id=1 s=30, id=2 s=30, id=3 s=5 - PARTITION BY grp works but ORDER BY id is ignored&lt;a class="anchor-link" aria-label="Link to heading" href="#test_start-result-returned-id1-s30-id2-s30-id3-s5---partition-by-grp-works-but-order-by-id-is-ignored"&gt;#&lt;/a&gt;&lt;/h1&gt;
&lt;h1 id="test_end-result-failed---named-window-order-by-id-is-not-applied"&gt;test_end result: Failed - named window ORDER BY id is not applied&lt;a class="anchor-link" aria-label="Link to heading" href="#test_end-result-failed---named-window-order-by-id-is-not-applied"&gt;#&lt;/a&gt;&lt;/h1&gt;
&lt;h3 id="final-result"&gt;final result&lt;a class="anchor-link" aria-label="Link to heading" href="#final-result"&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;h1 id="final-result-parsing-1-failed---named-window-reference-over-w-returns-full-partition-sum-for-grp1-row-id1-30-instead-of-running-value-10"&gt;final result: PARSING-1 failed - named window reference OVER w returns full-partition SUM for grp=1 row id=1 (30) instead of running value (10).&lt;a class="anchor-link" aria-label="Link to heading" href="#final-result-parsing-1-failed---named-window-reference-over-w-returns-full-partition-sum-for-grp1-row-id1-30-instead-of-running-value-10"&gt;#&lt;/a&gt;&lt;/h1&gt;
&lt;/details&gt;
&lt;p&gt;This issue was interesting for several reasons. First off, it’s a legitimate bug deep in our query processor, so it affected multiple products, including Dolt and Doltgres. It’s also syntax that we &lt;strong&gt;did&lt;/strong&gt; have test coverage for in our query processor. Unfortunately, the test coverage we had was asserting incorrect results! This is a great example of a latent bug where it &lt;em&gt;looked&lt;/em&gt; like we had good coverage, and we were indeed executing this syntax in tests, but we were asserting the wrong results. Ito provided great value here to help us catch this before a customer had to report it to us.&lt;/p&gt;
&lt;p&gt;This also illustrates a powerful aspect of Ito. Nothing in the code diff in the PR that Ito reviewed had &lt;strong&gt;any&lt;/strong&gt; direct sign of this bug. If Ito had only been looking at the diff lines from the PR, then it wouldn’t have caught this. Instead, Ito analyzed what was changing, used knowledge of window function syntax in SQL, and tested edge cases to see if it could find any problems, and sure enough, it did.&lt;/p&gt;
&lt;h2 id="bug-inheriting-or-overriding-a-named-windows-order-by-drops-it"&gt;Bug: Inheriting or overriding a named window’s &lt;code&gt;ORDER BY&lt;/code&gt; drops it&lt;a class="anchor-link" aria-label="Link to heading" href="#bug-inheriting-or-overriding-a-named-windows-order-by-drops-it"&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The next issue that Ito found is a similar, but separate bug. Just like the previous issue, this one is a bug in how a named window definition is merged into a statement, specifically when an &lt;code&gt;ORDER BY&lt;/code&gt; clause is provided in the statement to override the ordering in the named window definition. In this case, the overridden &lt;code&gt;ORDER BY&lt;/code&gt; clause wasn’t getting applied correctly, and resulted in the aggregate function being incorrectly applied over a window covering &lt;strong&gt;all&lt;/strong&gt; result rows, instead of using the correct window framing required by the &lt;code&gt;ORDER BY&lt;/code&gt; clause.&lt;/p&gt;
&lt;p&gt;Here’s a concrete example of this bug and how it affects the returned results:&lt;/p&gt;
&lt;pre class="astro-code github-dark" tabindex="0" data-language="sql"&gt;&lt;code&gt;&lt;span class="line"&gt;&lt;span&gt;CREATE&lt;/span&gt;&lt;span&gt; TABLE&lt;/span&gt;&lt;span&gt; t&lt;/span&gt;&lt;span&gt;(id &lt;/span&gt;&lt;span&gt;int&lt;/span&gt;&lt;span&gt;, grp &lt;/span&gt;&lt;span&gt;int&lt;/span&gt;&lt;span&gt;, amt &lt;/span&gt;&lt;span&gt;int&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;INSERT INTO&lt;/span&gt;&lt;span&gt; t &lt;/span&gt;&lt;span&gt;VALUES&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;10&lt;/span&gt;&lt;span&gt;),(&lt;/span&gt;&lt;span&gt;2&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;20&lt;/span&gt;&lt;span&gt;),(&lt;/span&gt;&lt;span&gt;3&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;30&lt;/span&gt;&lt;span&gt;),(&lt;/span&gt;&lt;span&gt;4&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;2&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;5&lt;/span&gt;&lt;span&gt;),(&lt;/span&gt;&lt;span&gt;5&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;2&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;15&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;SELECT&lt;/span&gt;&lt;span&gt; id, &lt;/span&gt;&lt;span&gt;SUM&lt;/span&gt;&lt;span&gt;(amt) &lt;/span&gt;&lt;span&gt;OVER&lt;/span&gt;&lt;span&gt; (w1 &lt;/span&gt;&lt;span&gt;ORDER BY&lt;/span&gt;&lt;span&gt; id) &lt;/span&gt;&lt;span&gt;AS&lt;/span&gt;&lt;span&gt; s &lt;/span&gt;&lt;span&gt;FROM&lt;/span&gt;&lt;span&gt; t&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;    WINDOW&lt;/span&gt;&lt;span&gt; w1 &lt;/span&gt;&lt;span&gt;AS&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;PARTITION&lt;/span&gt;&lt;span&gt; BY&lt;/span&gt;&lt;span&gt; grp) &lt;/span&gt;&lt;span&gt;ORDER BY&lt;/span&gt;&lt;span&gt; id;&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;-- buggy:   60, 60, 60, 20, 20   (full-partition sum, w1's frame leaking through)&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;-- correct: 10, 30, 60,  5, 20   (running sum, from the inline baseline SUM(amt) OVER (PARTITION BY grp ORDER BY id))&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here’s the comment Ito added for this issue:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.dolthub.com/blogimages/ito-issue-comment-named-window-inheritance-drops-order-by.png/60d609d34f9578a247cbee9b21ae41c1e00ad79fb3b7f9e65389329a710e159a.webp" alt="Ito issue comment named window inheritancedrops ORDER BY"&gt;&lt;/p&gt;
&lt;p&gt;As with the previous bug, Ito provided clear steps to reproduce this bug, which made it very easy to get started debugging it.&lt;/p&gt;
&lt;details&gt;
&lt;summary&gt;Click to see Ito's evidence for this bug&lt;/summary&gt;
&lt;h3 id="setup-and-reproduction-sql"&gt;setup and reproduction SQL&lt;a class="anchor-link" aria-label="Link to heading" href="#setup-and-reproduction-sql"&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;pre class="astro-code github-dark" tabindex="0" data-language="sql"&gt;&lt;code&gt;&lt;span class="line"&gt;&lt;span&gt;DROP&lt;/span&gt;&lt;span&gt; TABLE&lt;/span&gt;&lt;span&gt; IF&lt;/span&gt;&lt;span&gt; EXISTS&lt;/span&gt;&lt;span&gt; t_inherit;&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;CREATE&lt;/span&gt;&lt;span&gt; TABLE&lt;/span&gt;&lt;span&gt; t_inherit&lt;/span&gt;&lt;span&gt;(id &lt;/span&gt;&lt;span&gt;int&lt;/span&gt;&lt;span&gt;, grp &lt;/span&gt;&lt;span&gt;int&lt;/span&gt;&lt;span&gt;, amt &lt;/span&gt;&lt;span&gt;int&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;INSERT INTO&lt;/span&gt;&lt;span&gt; t_inherit &lt;/span&gt;&lt;span&gt;VALUES&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;10&lt;/span&gt;&lt;span&gt;),(&lt;/span&gt;&lt;span&gt;2&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;20&lt;/span&gt;&lt;span&gt;),(&lt;/span&gt;&lt;span&gt;3&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;30&lt;/span&gt;&lt;span&gt;),(&lt;/span&gt;&lt;span&gt;4&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;2&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;5&lt;/span&gt;&lt;span&gt;),(&lt;/span&gt;&lt;span&gt;5&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;2&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;15&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;-- Inheritance chain&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;SELECT&lt;/span&gt;&lt;span&gt; id, &lt;/span&gt;&lt;span&gt;SUM&lt;/span&gt;&lt;span&gt;(amt) &lt;/span&gt;&lt;span&gt;OVER&lt;/span&gt;&lt;span&gt; w2 &lt;/span&gt;&lt;span&gt;AS&lt;/span&gt;&lt;span&gt; s &lt;/span&gt;&lt;span&gt;FROM&lt;/span&gt;&lt;span&gt; t_inherit&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;  WINDOW&lt;/span&gt;&lt;span&gt; w1 &lt;/span&gt;&lt;span&gt;AS&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;PARTITION&lt;/span&gt;&lt;span&gt; BY&lt;/span&gt;&lt;span&gt; grp), w2 &lt;/span&gt;&lt;span&gt;AS&lt;/span&gt;&lt;span&gt; (w1 &lt;/span&gt;&lt;span&gt;ORDER BY&lt;/span&gt;&lt;span&gt; id) &lt;/span&gt;&lt;span&gt;ORDER BY&lt;/span&gt;&lt;span&gt; id;&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;-- Explicit named-window override&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;SELECT&lt;/span&gt;&lt;span&gt; id, &lt;/span&gt;&lt;span&gt;SUM&lt;/span&gt;&lt;span&gt;(amt) &lt;/span&gt;&lt;span&gt;OVER&lt;/span&gt;&lt;span&gt; (w1 &lt;/span&gt;&lt;span&gt;ORDER BY&lt;/span&gt;&lt;span&gt; id) &lt;/span&gt;&lt;span&gt;AS&lt;/span&gt;&lt;span&gt; s &lt;/span&gt;&lt;span&gt;FROM&lt;/span&gt;&lt;span&gt; t_inherit&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;  WINDOW&lt;/span&gt;&lt;span&gt; w1 &lt;/span&gt;&lt;span&gt;AS&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;PARTITION&lt;/span&gt;&lt;span&gt; BY&lt;/span&gt;&lt;span&gt; grp) &lt;/span&gt;&lt;span&gt;ORDER BY&lt;/span&gt;&lt;span&gt; id;&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;-- Inline baseline (control)&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;SELECT&lt;/span&gt;&lt;span&gt; id, &lt;/span&gt;&lt;span&gt;SUM&lt;/span&gt;&lt;span&gt;(amt) &lt;/span&gt;&lt;span&gt;OVER&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;PARTITION&lt;/span&gt;&lt;span&gt; BY&lt;/span&gt;&lt;span&gt; grp &lt;/span&gt;&lt;span&gt;ORDER BY&lt;/span&gt;&lt;span&gt; id) &lt;/span&gt;&lt;span&gt;AS&lt;/span&gt;&lt;span&gt; s &lt;/span&gt;&lt;span&gt;FROM&lt;/span&gt;&lt;span&gt; t_inherit &lt;/span&gt;&lt;span&gt;ORDER BY&lt;/span&gt;&lt;span&gt; id;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id="observed-output-1"&gt;observed output&lt;a class="anchor-link" aria-label="Link to heading" href="#observed-output-1"&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;h1 id="test-1-named-window-inheritance-chain-w1---w2"&gt;Test 1: Named window inheritance chain w1 -&gt; w2&lt;a class="anchor-link" aria-label="Link to heading" href="#test-1-named-window-inheritance-chain-w1---w2"&gt;#&lt;/a&gt;&lt;/h1&gt;
&lt;p&gt;id | s
----+----
1 | 60
2 | 60
3 | 60
4 | 20
5 | 20
(5 rows)&lt;/p&gt;
&lt;h1 id="test-2-named-window-with-override-clause"&gt;Test 2: Named window with override clause&lt;a class="anchor-link" aria-label="Link to heading" href="#test-2-named-window-with-override-clause"&gt;#&lt;/a&gt;&lt;/h1&gt;
&lt;p&gt;id | s
----+----
1 | 60
2 | 60
3 | 60
4 | 20
5 | 20
(5 rows)&lt;/p&gt;
&lt;h1 id="test-3-inline-window-baseline-correct-expected-behavior"&gt;Test 3: Inline window baseline (correct expected behavior)&lt;a class="anchor-link" aria-label="Link to heading" href="#test-3-inline-window-baseline-correct-expected-behavior"&gt;#&lt;/a&gt;&lt;/h1&gt;
&lt;p&gt;id | s
----+----
1 | 10
2 | 30
3 | 60
4 | 5
5 | 20
(5 rows)&lt;/p&gt;
&lt;h3 id="additional-mixed-form-readback-from-run"&gt;additional mixed-form readback from run&lt;a class="anchor-link" aria-label="Link to heading" href="#additional-mixed-form-readback-from-run"&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;id | s1 | s2 | s3
----+----+----+----
1 | 38 | 38 | 38
2 | 38 | 38 | 38
3 | 20 | 20 | 20
4 | 20 | 20 | 20
5 | 38 | 38 | 38
(5 rows)&lt;/p&gt;
&lt;h1 id="final-result-parsing-3-failed---named-window-inheritancereference-forms-return-full-partition-sums-while-equivalent-inline-over-partition-by-grp-order-by-id-returns-running-sums"&gt;final result: PARSING-3 failed - named-window inheritance/reference forms return full-partition sums while equivalent inline OVER (PARTITION BY grp ORDER BY id) returns running sums.&lt;a class="anchor-link" aria-label="Link to heading" href="#final-result-parsing-3-failed---named-window-inheritancereference-forms-return-full-partition-sums-while-equivalent-inline-over-partition-by-grp-order-by-id-returns-running-sums"&gt;#&lt;/a&gt;&lt;/h1&gt;
&lt;/details&gt;
&lt;p&gt;Like the previous bug, this bug was deep in our query processor, in the shared go-mysql-server module, so it affected all of our database products built on our query processor. Unlike the previous bug, this one was a gap in our test coverage. Thanks to Ito, we found this gap and added new tests for this case, ensuring that same tests will run for each of our database products and prevent a regression.&lt;/p&gt;
&lt;h1 id="not-just-for-sql"&gt;Not Just for SQL&lt;a class="anchor-link" aria-label="Link to heading" href="#not-just-for-sql"&gt;#&lt;/a&gt;&lt;/h1&gt;
&lt;p&gt;It’s worth noting that DoltgreSQL is not the typical product Ito can handle testing. Ito’s initial target was web applications: an agent drives a real browser against your app, clicks around like a user, and hands back a video replay of anything that broke, alongside the usual severity rating and repro steps. DoltgreSQL has no UI to click through. It’s a database engine that other programs talk to over the Postgres wire protocol. On our repo, Ito’s “user” is a SQL client instead of a browser, and its test scenarios are queries instead of clicks.&lt;/p&gt;
&lt;h1 id="ito-improvements"&gt;Ito Improvements&lt;a class="anchor-link" aria-label="Link to heading" href="#ito-improvements"&gt;#&lt;/a&gt;&lt;/h1&gt;
&lt;p&gt;We’ve only been using Ito for a little over a month, but even in that short amount of time, we’ve seen improvements in how well Ito is able to test our product. Like we mentioned earlier, the majority of Ito users are using it to test web-based applications. Because of that, Ito defaulted to taking screen recordings of the testing and showing that as the evidence for each bug report. As you can imagine… these screen recordings weren’t super interesting or helpful for a database server. The Ito team was receptive to this feedback and quickly rolled out changes so that non-UI products, like Doltgres, don’t include screen recordings, and instead, get a text file containing the evidence Ito used to determine each bug.&lt;/p&gt;
&lt;p&gt;We had another issue where Ito was reporting false alarms about SQL syntax that didn’t work. This was initially confusing because the PR where we saw this was successfully running tests that showed the new syntax working. When we reported this to the Ito team, they were responsive and quickly identified that Ito hit a problem building the Doltgres binary on our PR branch and instead used an older binary, which didn’t have support for the new syntax added in the PR. The Ito team quickly rolled out a change to prevent Ito from falling back to an older binary. Now if the binary couldn’t be successfully built from the PR branch, Ito would fail with a clear error message. We were happy with the quick response from the Ito team and haven’t seen this issue again.&lt;/p&gt;
&lt;p&gt;For the Doltgres database server, we’ve noticed that Ito does &lt;strong&gt;really&lt;/strong&gt; well finding issues with some PRs, but on other PRs, there are sometimes comments or reported issues that aren’t as helpful and can add noise to your PRs. How effectively Ito can find bugs in your PRs seems to depend on what type of application you’re building and what type of changes you have in your PR. For example, in our experience, we’ve noticed that Ito does &lt;strong&gt;really&lt;/strong&gt; well at finding bugs and broken edge cases when we’re implementing something well-documented, like features from the SQL spec. In the examples above, we were implementing well-defined syntax for SQL window functions, and Ito was able to identify a couple of broken edge cases. We’ve seen similarly helpful comments from Ito in other PRs where we implemented SQL functions, like &lt;code&gt;COALESCE()&lt;/code&gt;. In PRs where we were making internal performance optimizations that didn’t directly affect SQL features, Ito wasn’t as helpful or sometimes even added comments that didn’t warrant any action.&lt;/p&gt;
&lt;h1 id="by-the-numbers"&gt;By the Numbers&lt;a class="anchor-link" aria-label="Link to heading" href="#by-the-numbers"&gt;#&lt;/a&gt;&lt;/h1&gt;
&lt;p&gt;The examples above are illustrative of the kinds of issues Ito has found for us. In addition to that, I also want to share some stats on the overall effectiveness of Ito with our product so far. In the six weeks we’ve been using Ito, it has commented on 92 pull requests in the doltgresql repo. A little under half of those are dependabot-style PRs (e.g. automated dependency bumps, post-release metadata updates) with no real logic for Ito to exercise, so we’ll exclude those from our analysis. That leaves 43 substantive PRs where Ito actually had something to test.&lt;/p&gt;
&lt;p&gt;Of the 38 of those 43 PRs that have since merged or closed: 20 came back clean on every run, and on the 18 remaining PRs, Ito reported at least one issue. So, Ito is reporting issues for about half of our PRs. 8 of those 18 show a fix-and-re-verify cycle directly in Ito’s comment history before merge. That shows that for about half of the PRs where Ito is reporting issues, the PR author is seeing those issues and addressing them before merging the PR. This is still undercounting by a bit, since some Ito reported issues get fixed in a follow-up PR or moved to be tracked in our GitHub backlog. Based on that data, and rounding up a bit for issues that are addressed in follow-up PRs or added to the backlog, roughly 33% of our PRs are benefiting from the findings Ito is reporting.&lt;/p&gt;
&lt;p&gt;It’s also worth noting how often Ito finds bugs that have nothing to do with the PR it’s reviewing. In 26 of those 43 PRs (~60%), Ito surfaced at least one issue explicitly flagged as pre-existing and unrelated to the PR specific changes. Ito found those by understanding the functionality changing and testing edge cases around it, not because the PR touched that code path. That’s a different kind of value than catching a regression in the PR: it’s deeply testing parts of the product and finding existing issues that we didn’t know about yet.&lt;/p&gt;
&lt;p&gt;As one final angle, we went back through every review thread looking for cases where someone on the team weighed in directly on a specific Ito finding, calling it either legitimate or a false alarm. Across those judgment calls, we confirmed more than twice as many findings as we dismissed. A more than 2:1 ratio of confirmed-to-dismissed, on a tool that’s finding real bugs, is a worthwhile trade.&lt;/p&gt;
&lt;h1 id="summary"&gt;Summary&lt;a class="anchor-link" aria-label="Link to heading" href="#summary"&gt;#&lt;/a&gt;&lt;/h1&gt;
&lt;p&gt;We’ve been using the Ito automated review tool on PRs in the Doltgres repository for about six weeks now. After a few initial bumps getting our product working with Ito, which were all quickly resolved by the Ito team, we’ve been getting real value from the comments Ito leaves on our PRs. The numbers above bear that out, including a more than 2:1 signal-to-noise ratio on the findings we’ve actually sat down and judged. It’s been particularly helpful at catching broken edge cases in PRs related to SQL features. In this blog, we showed examples of Ito finding two bugs with SQL window framing, that both existed deep in our query processor dependency.&lt;/p&gt;
&lt;p&gt;We’re still experimenting with Ito and seeing where it is most helpful. When we’ve hit an occasional issue using the tool, the Ito team has been responsive and quick to roll out a solution to improve our experience, including making their product work well for non-web-UI products, like Doltgres.&lt;/p&gt;
&lt;p&gt;If you’re curious about Ito, you should try it out! It’s easy to &lt;a href="https://app.ito.ai/auth/signup?_gl=1*1hx2fzk*_gcl_au*MTAzNjIwNDM2OS4xNzgxNTY1Mzgw*_ga*MTgwMTA2NzQyNC4xNzg0MTQ4MzUx*_ga_BRWMBEWN9V*czE3ODQxNDgzNTEkbzEkZzEkdDE3ODQxNDgzNzUkajM2JGwwJGgw"&gt;create an account with Ito&lt;/a&gt; and hook up the ItoQA GitHub action to your GitHub repository. If you are hesitant to change your repository settings, you can also &lt;a href="https://www.ito.ai/review"&gt;try out Ito by analyzing a single PR&lt;/a&gt;. This gives you a really lightweight way to see the Ito experience on your own PR. You can find more details on &lt;a href="https://www.ito.ai/pricing"&gt;Ito’s pricing online&lt;/a&gt;, which includes options for a free trial, as well as free use for approved open-source projects.&lt;/p&gt;
&lt;p&gt;Last, but not least, if you want to talk about version-controlled databases, or AI tooling, please come by the &lt;a href="https://discord.gg/gqr7K4VNKe"&gt;DoltHub Discord&lt;/a&gt;. We’re always around Discord and happy to talk about the benefits of version-controlled databases and how AI tooling is changing the software development experience.&lt;/p&gt;</content:encoded>
      <dc:creator>Jason Fulghum</dc:creator>
      <category>ai</category>
      <category>doltgres</category>
    </item>
    <item>
      <title>Dolt in Four Flavors</title>
      <link>https://dolthub.com/blog/2026-07-16-dolt-in-4-flavors/</link>
      <guid isPermaLink="true">https://dolthub.com/blog/2026-07-16-dolt-in-4-flavors/</guid>
      <description>We now have the Dolt database that is right for you, whether that is classic Dolt, Doltgres, DoltLite, or Dumbo. Soon, all these databases will work with DoltHub, DoltLab, Hosted Dolt, and Dolt Workbench.</description>
      <pubDate>Thu, 16 Jul 2026 00:00:00 GMT</pubDate>
      <content:encoded>&lt;p&gt;We’ve been building Dolt for almost eight years now. The first seven of those years produced two Dolt flavors: classic &lt;a href="https://www.doltdb.com"&gt;MySQL-flavored Dolt&lt;/a&gt; and the much anticipated &lt;a href="https://www.doltgres.com"&gt;Postgres-flavored Doltgres&lt;/a&gt;. The past year has produced two additional flavors: the &lt;a href="https://www.doltlite.com"&gt;SQLite-flavored DoltLite&lt;/a&gt; and the &lt;a href="https://github.com/dolthub/dumbodb"&gt;MongoDB-flavored Dumbo&lt;/a&gt;. Coding agents rapidly accelerated our company’s ability to build new products. DoltLite and Dumbo are entirely agent-made.&lt;/p&gt;
&lt;p&gt;Why all the flavors? Which flavor should you choose for your use case? Which flavors work with other services like Hosted Dolt or DoltHub? This article explains.&lt;/p&gt;
&lt;h1 id="why-all-the-flavors"&gt;Why All the Flavors?&lt;a class="anchor-link" aria-label="Link to heading" href="#why-all-the-flavors"&gt;#&lt;/a&gt;&lt;/h1&gt;
&lt;p&gt;Version control is useful, and &lt;a href="https://www.dolthub.com/blog/2026-03-13-multi-agent-persistence/"&gt;essential if agents are involved&lt;/a&gt;, in every database format, and our product catalog now reflects that. Moreover, over the eight years we’ve been building Dolt, users have asked for almost every database flavor. There is demand for all manner of version-controlled databases.&lt;/p&gt;
&lt;p&gt;Of the four Dolt flavors, only Dolt and Doltgres are redundant. MySQL and Postgres are mostly interchangeable as Online Transaction Processing (OLTP) SQL databases. DoltLite and Dumbo have completely different form factors. DoltLite is an embedded database, not an OLTP database. Dumbo is an OLTP database but not a SQL database.&lt;/p&gt;
&lt;p&gt;So why Dolt and Doltgres? Database formats are very sticky. You are either a MySQL shop or a Postgres shop. Convincing people to switch database formats is harder than providing both options. Postgres has definitely become the default OLTP SQL format. This was not the case in 2018 when we started building Dolt. MySQL was still very popular. Today, not so much. A Postgres flavor is required in 2026. &lt;a href="https://www.dolthub.com/blog/2026-06-26-doltgres-1-0-coming-this-fall/"&gt;Doltgres goes 1.0&lt;/a&gt; in August, signaling it is ready for production use.&lt;/p&gt;
&lt;h1 id="which-flavor-is-right-for-you"&gt;Which Flavor Is Right for You?&lt;a class="anchor-link" aria-label="Link to heading" href="#which-flavor-is-right-for-you"&gt;#&lt;/a&gt;&lt;/h1&gt;
&lt;p&gt;Now that Dolt has four flavors, how do you pick the flavor that is right for you? This really comes down to what type of database you need.&lt;/p&gt;
&lt;p&gt;Do you want an OLTP database like MySQL or Postgres? Most OLTP databases are used to power the backends of websites or mobile applications. The database is run as a server that hosts one or many clients. If the answer to that question is yes, you have narrowed your choice down to Dolt, Doltgres, or Dumbo. If the answer is no, you probably want an embedded database and DoltLite is for you.&lt;/p&gt;
&lt;p&gt;Do you want your data structured as tables and accessed using SQL? If yes, then you can choose between Dolt and Doltgres. If no, Dumbo’s document format is for you.&lt;/p&gt;
&lt;p&gt;Choosing between Dolt and Doltgres comes down to your preference. Dolt is older and more stable. If you don’t care about SQL format, we always recommend choosing Dolt over Doltgres. If you are migrating from Postgres or wedded to Postgres as a company, choose Doltgres.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.dolthub.com/blogimages/dolt-flavor-flowchart.png/41295272a6d941c3a6b1091f22a45b073fda54c1e07b272d511eca2dc8d04cca.webp" alt="Flavor Decision"&gt;&lt;/p&gt;
&lt;h1 id="the-dolt-ecosystem"&gt;The Dolt Ecosystem&lt;a class="anchor-link" aria-label="Link to heading" href="#the-dolt-ecosystem"&gt;#&lt;/a&gt;&lt;/h1&gt;
&lt;p&gt;What about DoltHub, DoltLab, Hosted Dolt, and the Dolt Workbench? If I want to share my data on DoltHub, can I use Doltgres?&lt;/p&gt;
&lt;p&gt;The goal is for all four flavors of Dolt to work with DoltHub, DoltLab, Hosted Dolt, and the Dolt Workbench with one exception: DoltLite will not be hosted. However, we’re probably 6-12 months away from reaching that goal.&lt;/p&gt;
&lt;p&gt;With DoltHub and DoltLab, there are two components. Can you push to them as a remote? Can you view your database contents using the user interface? Supporting the first is pretty easy while supporting the second is more involved. User interface features are things like pull requests and SQL queries. Here’s a compatibility matrix with estimated dates for the unsupported cells.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.dolthub.com/blogimages/dolt-services-table.png/0f8d27091b117d12eeef39cf085cb198144f1e42863ade2c76698c37f0f78b53.webp" alt="Dolt Services Compatibility"&gt;&lt;/p&gt;
&lt;h1 id="conclusion"&gt;Conclusion&lt;a class="anchor-link" aria-label="Link to heading" href="#conclusion"&gt;#&lt;/a&gt;&lt;/h1&gt;
&lt;p&gt;We now have the Dolt that is right for you! Be that classic Dolt, Doltgres, DoltLite, or Dumbo. Soon, all these databases will work with DoltHub, DoltLab, Hosted Dolt, and Dolt Workbench. Want something prioritized? Come by &lt;a href="https://discord.gg/gqr7K4VNKe"&gt;our Discord&lt;/a&gt; and let us know. We’re always willing to shift the schedule based on user feedback.&lt;/p&gt;</content:encoded>
      <dc:creator>Tim Sehn</dc:creator>
      <category>dolt</category>
      <category>doltgres</category>
      <category>doltlite</category>
      <category>dumbo</category>
    </item>
    <item>
      <title>Dolt + TeamCity: Data Commits Now Trigger Builds</title>
      <link>https://dolthub.com/blog/2026-07-13-dolt-teamcity-vcs-plugin/</link>
      <guid isPermaLink="true">https://dolthub.com/blog/2026-07-13-dolt-teamcity-vcs-plugin/</guid>
      <description>A community plugin teaches TeamCity to treat a Dolt database as version control: data commits trigger builds, data tests, and binary packages.</description>
      <pubDate>Mon, 13 Jul 2026 00:00:00 GMT</pubDate>
      <content:encoded>&lt;p&gt;Here are two builds of the same tiny game, one database commit apart:&lt;/p&gt;
&lt;pre class="astro-code github-dark" tabindex="0" data-language="text"&gt;&lt;code&gt;&lt;span class="line"&gt;&lt;span&gt;=== Battle report: 3 units in the roster (config hnak4bdfk10s) ===&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;Footman vs Archer: draw at 6 hits each&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;=== Battle report: 3 units in the roster (config lravfogrck8d) ===&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;Footman vs Archer: Footman wins in 5 hits&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;No source code changed between these builds. A designer ran one SQL
&lt;code&gt;UPDATE&lt;/code&gt;, committed it, and a CI server was alerted to test and
ship a new binary with the commit hash stamped inside. The database is
&lt;a href="https://www.doltdb.com"&gt;Dolt&lt;/a&gt;, the world’s first version-controlled SQL
database. The CI server is
&lt;a href="https://www.jetbrains.com/teamcity/"&gt;TeamCity&lt;/a&gt;, which as of this year can
treat a Dolt database as a first-class version control system.&lt;/p&gt;
&lt;p&gt;It’s a collaborative sequel. Two years ago we wrote about
&lt;a href="https://www.dolthub.com/blog/2024-05-20-dolt-scorewarrior/"&gt;how Scorewarrior manages their game configuration with Dolt&lt;/a&gt;:
designers tune stats on branches, changes merge through pull requests,
and shipping means cutting a tag and building binary artifacts. That post
ended right where the config leaves Dolt and enters the build system.
Now the engineer who ran that build system just fixed its one missing piece.&lt;/p&gt;
&lt;h1 id="what-is-teamcity"&gt;What Is TeamCity?&lt;a class="anchor-link" aria-label="Link to heading" href="#what-is-teamcity"&gt;#&lt;/a&gt;&lt;/h1&gt;
&lt;p&gt;TeamCity is JetBrains’ continuous integration (CI) server for developers and
build engineers. If CI is new to
you: it’s the practice of committing changes to a shared repository many
times a day, with every commit followed by an automated build and test
run, so integration problems surface in minutes instead of at release
time. TeamCity is a veteran of the field, available on-premises or as a
managed cloud service, and the free
&lt;a href="https://www.jetbrains.com/teamcity/buy/?edition=on-premises"&gt;Professional license&lt;/a&gt; is
generous: 100 build configurations, 3 build agents, no restrictions on
users or runtime, and the full feature set for commercial, open-source,
and personal projects alike.&lt;/p&gt;
&lt;p&gt;The architecture has two primary components: the TeamCity server, which
provides the web UI for managing configuration and results, and build
agents, the workers that execute your builds. You configure everything
through the UI, through the
&lt;a href="https://www.jetbrains.com/help/teamcity/kotlin-dsl.html"&gt;Kotlin DSL&lt;/a&gt;, or
a hybrid of both, with UI changes committed back to your repository as
code.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.dolthub.com/blogimages/teamcity-gameconfig-project-overview.png/1000f25bd4fda31f9919494f52548b7ab0abe6feb736f85281a3d9ce3ff0ed3a.webp" alt="TeamCity&amp;#x27;s overview page for our GameConfig project, with its build configuration and recent runs"&gt;&lt;/p&gt;
&lt;p&gt;Notice the word “repository”. TeamCity is built around version control:
it polls your VCS for commits, lists them in a Changes tab, triggers
builds when they land, and pins every build to the revision that caused
it. That raises our favorite kind of question: what if the thing under
version control is a database?&lt;/p&gt;
&lt;h1 id="teamcity-and-dolt-before"&gt;TeamCity and Dolt, Before&lt;a class="anchor-link" aria-label="Link to heading" href="#teamcity-and-dolt-before"&gt;#&lt;/a&gt;&lt;/h1&gt;
&lt;p&gt;&lt;a href="https://www.linkedin.com/in/prodoelmit/"&gt;Yury Dynnikov&lt;/a&gt;, formerly of
Scorewarrior and now at JetBrains, spent years building game configs from
Dolt into binary packages with TeamCity. However, TeamCity did not
understand Dolt’s version control: commits, branches, and tags were all
invisible. Still, a pipeline could work with a separate replica server
just to have something stable to build from, branch names passed around
as build parameters instead of TeamCity’s native branch handling, and
some other not-so-cool tricks.&lt;/p&gt;
&lt;h1 id="look-a-cool-new-plugin"&gt;Look, a Cool New Plugin!&lt;a class="anchor-link" aria-label="Link to heading" href="#look-a-cool-new-plugin"&gt;#&lt;/a&gt;&lt;/h1&gt;
&lt;p&gt;Yury’s fix is a
&lt;a href="https://plugins.jetbrains.com/plugin/31918-vcs-support-dolt"&gt;Dolt plugin for TeamCity&lt;/a&gt;
(&lt;a href="https://github.com/prodoelmit/teamcity-dolt"&gt;source on GitHub&lt;/a&gt;, MIT
licensed). He is clear it’s a personal project rather than a JetBrains
product, but it does what his old pipeline always wanted: install it and
“Dolt” appears in TeamCity’s Type of VCS dropdown, right next to Git,
Subversion, and Perforce.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.dolthub.com/blogimages/teamcity-type-of-vcs-dropdown-showing-dolt.png/15678f621b7132c00daca0d6db608db30a18fff1f31fc97cd8a6f413611e8785.webp" alt="TeamCity&amp;#x27;s Type of VCS dropdown listing Dolt alongside Git, Subversion, and Perforce"&gt;&lt;/p&gt;
&lt;p&gt;Commits show up in the Changes tab with hash, author, and message. Branch
specifications work, so a commit to &lt;code&gt;staging&lt;/code&gt; builds apart from &lt;code&gt;main&lt;/code&gt;,
each branch with its own build history:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.dolthub.com/blogimages/teamcity-build-config-overview-with-branch-builds.png/6932c417bfabff845ffc0452252d985fcc4370f1e5837769edd4e0be475ec461.webp" alt="The build overview showing runs from the main and staging branches side by side"&gt;&lt;/p&gt;
&lt;p&gt;There are two connection modes: Remote JDBC
points at a &lt;code&gt;dolt sql-server&lt;/code&gt; you already run, while Local mode clones
from &lt;a href="https://www.dolthub.com"&gt;DoltHub&lt;/a&gt; or &lt;a href="https://www.doltlab.com"&gt;DoltLab&lt;/a&gt;
and manages its own SQL server, with private repos handled through an
Ed25519 keypair flow, the same
&lt;a href="https://www.dolthub.com/blog/2025-08-26-debugging-dolt-login/"&gt;credential mechanism &lt;code&gt;dolt login&lt;/code&gt; uses&lt;/a&gt;,
except TeamCity generates the keypair and you register its public half on
DoltHub.&lt;/p&gt;
&lt;p&gt;Yury also runs a public
&lt;a href="https://teamcity-dolt-demo.prodoelmit.me/project/EndlessSky"&gt;demo server&lt;/a&gt;
(guest login) where the plugin builds the
&lt;a href="https://endless-sky.github.io/"&gt;Endless Sky&lt;/a&gt; game data through a real
pipeline: export, data-quality tests, release bundles, all configured in
Kotlin DSL.&lt;/p&gt;
&lt;h1 id="the-query-behind-the-changes-tab"&gt;The Query Behind the Changes Tab&lt;a class="anchor-link" aria-label="Link to heading" href="#the-query-behind-the-changes-tab"&gt;#&lt;/a&gt;&lt;/h1&gt;
&lt;p&gt;The plugin renders data changes and schema changes as different entries,
and you can see how via a common Dolt SQL function:&lt;/p&gt;
&lt;pre class="astro-code github-dark" tabindex="0" data-language="sql"&gt;&lt;code&gt;&lt;span class="line"&gt;&lt;span&gt;SELECT&lt;/span&gt;&lt;span&gt; *&lt;/span&gt;&lt;span&gt; FROM&lt;/span&gt;&lt;span&gt; DOLT_DIFF_SUMMARY(&lt;/span&gt;&lt;span&gt;'hnak4bdfk10s...'&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;'lravfogrck8d...'&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt;-----------------+---------------+-----------+-------------+---------------+&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;| from_table_name | to_table_name | diff_type | data_change | schema_change |&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt;-----------------+---------------+-----------+-------------+---------------+&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;| units           | units         | modified  | &lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;           | &lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;             |&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt;-----------------+---------------+-----------+-------------+---------------+&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A &lt;code&gt;data_change&lt;/code&gt; without &lt;code&gt;schema_change&lt;/code&gt; makes the Changes tab show
&lt;code&gt;tables/units&lt;/code&gt;. But run the same function against our branch that adds an
&lt;code&gt;armor&lt;/code&gt; column and both flags flip on, so the tab renders
&lt;code&gt;tables/units (schema)&lt;/code&gt; as its own entry.&lt;/p&gt;
&lt;p&gt;Why bring this up? Risk. The Footman buff changes a
number the game already knows how to read, so CI can test it and ship it
to live servers right away. The &lt;code&gt;armor&lt;/code&gt; column changes the shape of the
data, and every program that reads the config now has to be updated to
match, so that commit should get extra checks.&lt;/p&gt;
&lt;p&gt;Since the plugin writes the two as different paths, TeamCity
&lt;a href="https://www.jetbrains.com/help/teamcity/configuring-vcs-triggers.html"&gt;trigger rules&lt;/a&gt;
can route each kind to the right build automatically.&lt;/p&gt;
&lt;h1 id="setup"&gt;Setup&lt;a class="anchor-link" aria-label="Link to heading" href="#setup"&gt;#&lt;/a&gt;&lt;/h1&gt;
&lt;p&gt;&lt;a href="https://www.jetbrains.com/teamcity/download/"&gt;Installers are available for Linux, macOS, and Windows&lt;/a&gt;,
but we used
&lt;a href="https://hub.docker.com/r/jetbrains/teamcity-server/"&gt;JetBrains’ own recommended Docker distribution&lt;/a&gt;.
After all, this is an inherently multi-service system: the TeamCity
server needs the
&lt;a href="https://hub.docker.com/r/jetbrains/teamcity-agent/"&gt;TeamCity agent image&lt;/a&gt;
to spawn build processes, plus a
&lt;a href="https://hub.docker.com/r/dolthub/dolt-sql-server"&gt;Dolt SQL server&lt;/a&gt; to be
the repository. (New to Dolt in Docker? We have a
&lt;a href="https://www.dolthub.com/blog/2023-10-25-dolt-docker/"&gt;getting started post&lt;/a&gt;.)
Three containers, wired like this:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.dolthub.com/blogimages/teamcity-docker-compose-architecture-mermaid-flowchart.png/02730e527918c4a4b5adabdf03ee3267a55a30a16b98563cce78043f1264ce70.webp" alt="Diagram of the three-container stack: browser to TeamCity server, server polling Dolt over JDBC, agent registering and querying"&gt;&lt;/p&gt;
&lt;p&gt;This Compose file is everything you need to follow along:&lt;/p&gt;
&lt;pre class="astro-code github-dark" tabindex="0" data-language="yaml"&gt;&lt;code&gt;&lt;span class="line"&gt;&lt;span&gt;services&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;  dolt&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;    image&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;dolthub/dolt-sql-server:latest&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;    environment&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;      DOLT_ROOT_PASSWORD&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;dolt&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;      DOLT_ROOT_HOST&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;"%"&lt;/span&gt;&lt;span&gt;    # let other containers connect as root&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;    ports&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;      - &lt;/span&gt;&lt;span&gt;"3306:3306"&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;  server&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;    image&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;jetbrains/teamcity-server:latest&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;    ports&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;      - &lt;/span&gt;&lt;span&gt;"8111:8111"&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;    volumes&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;      - &lt;/span&gt;&lt;span&gt;tc_data:/data/teamcity_server/datadir&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;      - &lt;/span&gt;&lt;span&gt;tc_logs:/opt/teamcity/logs&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;  agent&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;    image&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;jetbrains/teamcity-agent:latest&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;    environment&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;      SERVER_URL&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;http://server:8111&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;volumes&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;  tc_data&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;  tc_logs&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Run &lt;code&gt;docker compose up -d&lt;/code&gt;, give TeamCity a couple of minutes to boot,
then open &lt;code&gt;http://localhost:8111&lt;/code&gt; and click through the one-time wizard:
accept the defaults (the internal database is fine for evaluation),
create your admin user, and authorize the agent under Agents,
Unauthorized. Next, install the plugin: download the zip from the
&lt;a href="https://plugins.jetbrains.com/plugin/31918-vcs-support-dolt"&gt;Marketplace page&lt;/a&gt;,
upload it on the Plugins page behind the Admin gear in the left-hand
navigation bar, and restart the server. You need TeamCity 2025.11.3 or
newer.&lt;/p&gt;
&lt;p&gt;While that restarts, give Dolt something worth building. Connect with
any MySQL client (&lt;code&gt;mysql -h127.0.0.1 -uroot -pdolt&lt;/code&gt;) and seed a tiny
game config:&lt;/p&gt;
&lt;pre class="astro-code github-dark" tabindex="0" data-language="sql"&gt;&lt;code&gt;&lt;span class="line"&gt;&lt;span&gt;CREATE&lt;/span&gt;&lt;span&gt; DATABASE&lt;/span&gt;&lt;span&gt; gameconfig&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;USE&lt;/span&gt;&lt;span&gt; gameconfig;&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;CREATE&lt;/span&gt;&lt;span&gt; TABLE&lt;/span&gt;&lt;span&gt; units&lt;/span&gt;&lt;span&gt; (id &lt;/span&gt;&lt;span&gt;INT&lt;/span&gt;&lt;span&gt; PRIMARY KEY&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;name&lt;/span&gt;&lt;span&gt; VARCHAR&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;64&lt;/span&gt;&lt;span&gt;), hp &lt;/span&gt;&lt;span&gt;INT&lt;/span&gt;&lt;span&gt;, attack &lt;/span&gt;&lt;span&gt;INT&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;INSERT INTO&lt;/span&gt;&lt;span&gt; units &lt;/span&gt;&lt;span&gt;VALUES&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;'Footman'&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;100&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;12&lt;/span&gt;&lt;span&gt;),(&lt;/span&gt;&lt;span&gt;2&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;'Archer'&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;70&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;18&lt;/span&gt;&lt;span&gt;),(&lt;/span&gt;&lt;span&gt;3&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;'Knight'&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;180&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;25&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;CALL&lt;/span&gt;&lt;span&gt; DOLT_COMMIT(&lt;/span&gt;&lt;span&gt;'-Am'&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;'Initial game config'&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Finally, connect the two. Create a project (TeamCity’s onboarding will
offer to connect VCS hosting accounts and set up pipelines along the way;
skip all of it, because your version control is the database), add a VCS root of
type “Dolt”, and fill in the form: connection mode Remote JDBC, host
&lt;code&gt;dolt&lt;/code&gt; (the Compose service name), port &lt;code&gt;3306&lt;/code&gt;, database &lt;code&gt;gameconfig&lt;/code&gt;,
user &lt;code&gt;root&lt;/code&gt;, password &lt;code&gt;dolt&lt;/code&gt;, and branch specification &lt;code&gt;+:*&lt;/code&gt; to watch
every branch. Test connection, green.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.dolthub.com/blogimages/teamcity-new-vcs-root-page-all-filled-values.png/c75d8ea7619b4b168ac3c9cc45eb5b7ae9c0aeccbcccc5b5439752075201e279.webp" alt="The Dolt VCS root form filled in with Remote JDBC mode, host dolt, database gameconfig, and branch specification watching all branches"&gt;&lt;/p&gt;
&lt;p&gt;One last piece of plumbing: a build configuration, so those commits have
something to trigger. Click “Create build configuration”, and on the “Set
up your build” page keep the plain “Build configuration” option and pick
your &lt;code&gt;gameconfig-dolt&lt;/code&gt; root under “From an existing VCS root”:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.dolthub.com/blogimages/teamcity-set-up-new-build.png/b97511486a1865bf2fb41f9f1b2cc2cf570b0e5f06511ce849db7be196dd4fd7.webp" alt="The Set up your build page with the Build configuration option and the gameconfig-dolt root selected"&gt;&lt;/p&gt;
&lt;p&gt;Give it a Command Line build step; even &lt;code&gt;dolt version&lt;/code&gt; works as a
placeholder while you wire things up. Then add a
&lt;a href="https://www.jetbrains.com/help/teamcity/configuring-vcs-triggers.html"&gt;VCS trigger&lt;/a&gt;
under Triggers so new commits start builds on their own:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.dolthub.com/blogimages/teamcity-build-config-triggers.png/b2f2c50b7a64fc0b1c5200d5ec5cb0fd27a4b2b2979476221ceb35be91b86ff7.webp" alt="The Triggers page with a VCS trigger added to the build configuration"&gt;&lt;/p&gt;
&lt;p&gt;The steps our demo build runs are more specific (i.e., data-quality probes
and compilation) which will come up in later sections.&lt;/p&gt;
&lt;h1 id="a-data-commit-walks-into-a-build"&gt;A Data Commit Walks Into a Build&lt;a class="anchor-link" aria-label="Link to heading" href="#a-data-commit-walks-into-a-build"&gt;#&lt;/a&gt;&lt;/h1&gt;
&lt;p&gt;Here is the whole chain, end to end:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.dolthub.com/blogimages/teamcity-data-commit-mermaid-sequence-diagram.png/f2d3360253be005131c2a90e05fddde9af5303ea486a125701b5147761b55082.webp" alt="Sequence diagram: a data commit, TeamCity&amp;#x27;s poll finding it, the VCS trigger firing, and the build querying data at that commit"&gt;&lt;/p&gt;
&lt;p&gt;Let’s say a designer is buffing a stat, expressed as SQL against the running
server:&lt;/p&gt;
&lt;pre class="astro-code github-dark" tabindex="0" data-language="sql"&gt;&lt;code&gt;&lt;span class="line"&gt;&lt;span&gt;CALL&lt;/span&gt;&lt;span&gt; DOLT_CHECKOUT(&lt;/span&gt;&lt;span&gt;'main'&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;UPDATE&lt;/span&gt;&lt;span&gt; units &lt;/span&gt;&lt;span&gt;SET&lt;/span&gt;&lt;span&gt; attack &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; 14&lt;/span&gt;&lt;span&gt; WHERE&lt;/span&gt;&lt;span&gt; name&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; 'Footman'&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;CALL&lt;/span&gt;&lt;span&gt; DOLT_COMMIT(&lt;/span&gt;&lt;span&gt;'-Am'&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;'Balance pass: buff Footman attack 12 -&gt; 14'&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;About a minute later the commit is in the Changes tab and the VCS trigger
has started a build. A row update just triggered continuous integration.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.dolthub.com/blogimages/teamcity-build-config-changes-tab-success.png/fbecb0d09855ddfcd0bfee32e00e84baa92957f836f5c0c7c6dfec4933e9844d.webp" alt="The build&amp;#x27;s Changes tab showing the Dolt commit with its hash, author, and message, rendered like any Git commit"&gt;&lt;/p&gt;
&lt;p&gt;TeamCity hands every build the commit that triggered it as
&lt;code&gt;%build.vcs.number%&lt;/code&gt;, and Dolt can
&lt;a href="https://www.dolthub.com/docs/sql-reference/version-control/querying-history/"&gt;query any table as of any commit&lt;/a&gt;.
Build steps therefore read the data exactly as it was at the triggering
revision, even if someone commits again while the build waits in the
queue:&lt;/p&gt;
&lt;pre class="astro-code github-dark" tabindex="0" data-language="bash"&gt;&lt;code&gt;&lt;span class="line"&gt;&lt;span&gt;dolt&lt;/span&gt;&lt;span&gt; sql&lt;/span&gt;&lt;span&gt; -q&lt;/span&gt;&lt;span&gt; "SELECT * FROM units AS OF '%build.vcs.number%'"&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h1 id="watch-a-test-turn-red"&gt;Watch a Test Turn Red&lt;a class="anchor-link" aria-label="Link to heading" href="#watch-a-test-turn-red"&gt;#&lt;/a&gt;&lt;/h1&gt;
&lt;p&gt;Our build step runs data-quality probes before anything ships, each
reported through TeamCity
&lt;a href="https://www.jetbrains.com/help/teamcity/service-messages.html"&gt;service messages&lt;/a&gt;
so a bad commit fails the build as a named red test instead of a wall of
log text, and the step halts on a red probe so bad config never reaches
the compiler. Here is the entire mechanism, two probes and a gate:&lt;/p&gt;
&lt;pre class="astro-code github-dark" tabindex="0" data-language="bash"&gt;&lt;code&gt;&lt;span class="line"&gt;&lt;span&gt;PROBES_FAILED&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;probe&lt;/span&gt;&lt;span&gt;() {&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;  echo&lt;/span&gt;&lt;span&gt; "##teamcity[testStarted name='&lt;/span&gt;&lt;span&gt;$1&lt;/span&gt;&lt;span&gt;']"&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;  bad&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;$(&lt;/span&gt;&lt;span&gt;dolt&lt;/span&gt;&lt;span&gt; --host&lt;/span&gt;&lt;span&gt; dolt&lt;/span&gt;&lt;span&gt; --port&lt;/span&gt;&lt;span&gt; 3306&lt;/span&gt;&lt;span&gt; -u&lt;/span&gt;&lt;span&gt; root&lt;/span&gt;&lt;span&gt; -p&lt;/span&gt;&lt;span&gt; dolt&lt;/span&gt;&lt;span&gt; --use-db&lt;/span&gt;&lt;span&gt; gameconfig&lt;/span&gt;&lt;span&gt; --no-tls&lt;/span&gt;&lt;span&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;    sql&lt;/span&gt;&lt;span&gt; -q&lt;/span&gt;&lt;span&gt; "&lt;/span&gt;&lt;span&gt;$2&lt;/span&gt;&lt;span&gt;"&lt;/span&gt;&lt;span&gt; -r&lt;/span&gt;&lt;span&gt; csv&lt;/span&gt;&lt;span&gt; |&lt;/span&gt;&lt;span&gt; tail&lt;/span&gt;&lt;span&gt; -1&lt;/span&gt;&lt;span&gt; |&lt;/span&gt;&lt;span&gt; tr&lt;/span&gt;&lt;span&gt; -d&lt;/span&gt;&lt;span&gt; '\r'&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;  if&lt;/span&gt;&lt;span&gt; [ &lt;/span&gt;&lt;span&gt;"${&lt;/span&gt;&lt;span&gt;bad&lt;/span&gt;&lt;span&gt;:-&lt;/span&gt;&lt;span&gt;error&lt;/span&gt;&lt;span&gt;}"&lt;/span&gt;&lt;span&gt; !=&lt;/span&gt;&lt;span&gt; "0"&lt;/span&gt;&lt;span&gt; ]; &lt;/span&gt;&lt;span&gt;then&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;    echo&lt;/span&gt;&lt;span&gt; "##teamcity[testFailed name='&lt;/span&gt;&lt;span&gt;$1&lt;/span&gt;&lt;span&gt;' message='${&lt;/span&gt;&lt;span&gt;bad&lt;/span&gt;&lt;span&gt;:-&lt;/span&gt;&lt;span&gt;probe&lt;/span&gt;&lt;span&gt; query&lt;/span&gt;&lt;span&gt; failed&lt;/span&gt;&lt;span&gt;} offending rows']"&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;    PROBES_FAILED&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;  fi&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;  echo&lt;/span&gt;&lt;span&gt; "##teamcity[testFinished name='&lt;/span&gt;&lt;span&gt;$1&lt;/span&gt;&lt;span&gt;']"&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;probe&lt;/span&gt;&lt;span&gt; "data.units.positive_stats"&lt;/span&gt;&lt;span&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;  "SELECT COUNT(*) FROM units AS OF '%build.vcs.number%' WHERE hp &amp;#x3C;= 0 OR attack &amp;#x3C;= 0"&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;probe&lt;/span&gt;&lt;span&gt; "data.units.unique_names"&lt;/span&gt;&lt;span&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;  "SELECT COUNT(*) - COUNT(DISTINCT name) FROM units AS OF '%build.vcs.number%'"&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; [ &lt;/span&gt;&lt;span&gt;"&lt;/span&gt;&lt;span&gt;$PROBES_FAILED&lt;/span&gt;&lt;span&gt;"&lt;/span&gt;&lt;span&gt; !=&lt;/span&gt;&lt;span&gt; "0"&lt;/span&gt;&lt;span&gt; ]; &lt;/span&gt;&lt;span&gt;then&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;  echo&lt;/span&gt;&lt;span&gt; "Data-quality probes failed; skipping compile and package."&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;  exit&lt;/span&gt;&lt;span&gt; 1&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;fi&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A probe is a &lt;code&gt;COUNT(*)&lt;/code&gt; of rows that should not exist, and any nonzero answer becomes a
failed test. We committed a unit with attack zero to showcase:&lt;/p&gt;
&lt;pre class="astro-code github-dark" tabindex="0" data-language="text"&gt;&lt;code&gt;&lt;span class="line"&gt;&lt;span&gt;##teamcity[testStarted name='data.units.positive_stats']&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;##teamcity[testFailed name='data.units.positive_stats' message='1 offending rows']&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;##teamcity[testFinished name='data.units.positive_stats']&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The build goes red in the overview:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.dolthub.com/blogimages/teamcity-build-config-overview-with-fail-build.png/e311dc3d07b7e741a381fdbf7eb875e6cf5fbc69cee3731af2fb183b69688712.webp" alt="The build overview with the Bugbear commit&amp;#x27;s build failed and marked red"&gt;&lt;/p&gt;
&lt;p&gt;Opening the failed build shows the step halted by the gate:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.dolthub.com/blogimages/teamcity-build-config-failing-build-instance-overview.png/4ae0c994fa0c5fa95a69969638f62b25314c6d9eccdc325bd45921c3d1d00cd7.webp" alt="The failed build&amp;#x27;s page, its command line step stopped after the probes reported bad data"&gt;&lt;/p&gt;
&lt;p&gt;The Tests tab names the exact check that caught it, and nothing gets
packaged:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.dolthub.com/blogimages/teamcity-data-units-positive-stats-failed-test.png/c1a72d1469a027f4595f8ea495cc9ecf911b76b6f0825fed644f708333d514e9.webp" alt="The Tests tab showing the data.units.positive_stats probe failed with one offending row"&gt;&lt;/p&gt;
&lt;p&gt;Revert the commit (&lt;code&gt;CALL DOLT_REVERT('HEAD')&lt;/code&gt;) and the revert, being
itself a commit, triggers one more build. That one is green.&lt;/p&gt;
&lt;h1 id="two-builds-two-games-one-commit-apart"&gt;Two Builds, Two Games, One Commit Apart&lt;a class="anchor-link" aria-label="Link to heading" href="#two-builds-two-games-one-commit-apart"&gt;#&lt;/a&gt;&lt;/h1&gt;
&lt;p&gt;Our build step generates Go source from the config at the pinned revision,
compiles an actual game binary with the Dolt commit stamped inside via
ldflags, and cross-compiles it for Linux, Windows, and Mac from one Linux
agent. The binaries self-identify: the &lt;code&gt;config hnak4bdfk10s&lt;/code&gt; in that
first line of output is the real Dolt commit hash the artifact carries.&lt;/p&gt;
&lt;p&gt;They are also reproducible. Building the same commit twice yields
byte-identical output:&lt;/p&gt;
&lt;pre class="astro-code github-dark" tabindex="0" data-language="text"&gt;&lt;code&gt;&lt;span class="line"&gt;&lt;span&gt;build1 sha256: a1d97cf4d47502e5...&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;build2 sha256: a1d97cf4d47502e5...&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Dolt pins the data, the build pins everything else, and any artifact in
the wild traces back to the exact data commit that produced it. The
build’s Artifacts tab lists the binaries, each named after its revision:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.dolthub.com/blogimages/teamcity-build-config-success-artifacts.png/9c4e7bb8181a52afa54a813425bf31e0b37571a5bc5744d3dd3dcf12a14cfbc7.webp" alt="The build&amp;#x27;s Artifacts tab listing game binaries for Linux, Windows, and Mac, each named with the Dolt commit hash"&gt;&lt;/p&gt;
&lt;h1 id="the-fine-print"&gt;The Fine Print&lt;a class="anchor-link" aria-label="Link to heading" href="#the-fine-print"&gt;#&lt;/a&gt;&lt;/h1&gt;
&lt;p&gt;The plugin is young, and its documentation is upfront about the edges:
data changes currently show row
counts rather than row-level diffs, the Changes tab is a linear list
without a commit graph, and there is no write-back yet, so no merges or
pre-tested commits driven from TeamCity. That last item is on the
&lt;a href="https://github.com/prodoelmit/teamcity-dolt/blob/master/docs/limitations-and-roadmap.md"&gt;roadmap&lt;/a&gt;,
and it’s the one to watch: CI gating merges to your data the way it
gates merges to your code.&lt;/p&gt;
&lt;p&gt;If that idea appeals, and you would rather not run your own CI server, we
have been building it from the hosted side for a while: DoltHub supports
&lt;a href="https://www.dolthub.com/blog/2024-11-14-continuous-integration-on-data/"&gt;CI testing on data&lt;/a&gt;
and &lt;a href="https://www.dolthub.com/blog/2024-12-12-pull-request-ci-on-dolthub/"&gt;pull request CI&lt;/a&gt;,
and we were wiring
&lt;a href="https://www.dolthub.com/blog/2020-04-08-data-ci-with-dolthub-webhooks/"&gt;data CI out of webhooks&lt;/a&gt;
back in 2020. Yury’s plugin brings the same conviction to your own
TeamCity, where your builds may already live.&lt;/p&gt;
&lt;h1 id="conclusion"&gt;Conclusion&lt;a class="anchor-link" aria-label="Link to heading" href="#conclusion"&gt;#&lt;/a&gt;&lt;/h1&gt;
&lt;p&gt;The Scorewarrior post asked what you would use a version-controlled SQL
database for. This plugin answers one layer up the stack: you build from
it, test against it, and ship artifacts traceable to it, with your CI
server treating data commits as what they are, commits. Give the
&lt;a href="https://plugins.jetbrains.com/plugin/31918-vcs-support-dolt"&gt;plugin&lt;/a&gt; a
spin, poke at the
&lt;a href="https://teamcity-dolt-demo.prodoelmit.me/project/EndlessSky"&gt;demo server&lt;/a&gt;,
and tell Yury what you think. He asked for ideas on showcasing it, and
when we checked, the download counter read five. Let’s fix that.&lt;/p&gt;
&lt;!-- Check the Marketplace download count:
     https://plugins.jetbrains.com/api/plugins/31918/updates --&gt;
&lt;p&gt;Curious about version-controlled databases, or want to talk CI on data?
Stop by the &lt;a href="https://discord.gg/gqr7K4VNKe"&gt;DoltHub Discord&lt;/a&gt; and say
hello. Our engineering team hangs out there all day.&lt;/p&gt;</content:encoded>
      <dc:creator>Elian Deogracia-Brito</dc:creator>
      <category>integration</category>
      <category>use case</category>
      <category>dolt</category>
    </item>
    <item>
      <title>Doltgres Reaches 99% Compliance on SQL Logic Tests</title>
      <link>https://dolthub.com/blog/2026-07-10-doltgres-99-percent-sql-logic-tests/</link>
      <guid isPermaLink="true">https://dolthub.com/blog/2026-07-10-doltgres-99-percent-sql-logic-tests/</guid>
      <description>Doltgres now passes 99% of our SQL Logic Test suite, hitting one of our key correctness targets for the Doltgres 1.0 launch on August 6th.</description>
      <pubDate>Fri, 10 Jul 2026 00:00:00 GMT</pubDate>
      <content:encoded>&lt;p&gt;Two weeks ago, we &lt;a href="https://www.dolthub.com/blog/2026-06-26-doltgres-1-0-coming-this-fall/"&gt;announced that Doltgres 1.0 is coming August 6th&lt;/a&gt;. In that post, we laid out the four things we’re focused on to get there: correctness, storage format stability, performance, and compatibility. Correctness was measured by one very concrete number: &lt;strong&gt;99% compliance on our SQL Logic Test suite&lt;/strong&gt;. At the time we were sitting at a little over 96%. Today, we’ve hit our target: Doltgres passes 99% of the suite. That’s one more box checked on the road to 1.0. 🎉&lt;/p&gt;
&lt;h2 id="sql-logic-test"&gt;SQL Logic Test&lt;a class="anchor-link" aria-label="Link to heading" href="#sql-logic-test"&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://sqlite.org/sqllogictest/doc/trunk/about.wiki"&gt;SQL Logic Test&lt;/a&gt; is a test suite originally built for SQLite, containing millions of statements and queries that exercise SQL expressions, joins, aggregates, and type coercion rules. We forked it &lt;a href="https://www.dolthub.com/blog/2023-11-27-doltgres-sqllogic-test/"&gt;years ago&lt;/a&gt; and extended it with more tests to measure how correctly Dolt (and now Doltgres) execute SQL statements. The test suite in SQL Logic Test specifically stress tests the expression support in each engine. 99% represents millions of individual queries whose results have to match PostgreSQL exactly, down to the type and formatting of every returned value. This gives us a high confidence that Doltgres can correctly execute a wide range of statements and expressions.&lt;/p&gt;
&lt;h2 id="establishing-a-baseline-against-postgresql"&gt;Establishing a Baseline Against PostgreSQL&lt;a class="anchor-link" aria-label="Link to heading" href="#establishing-a-baseline-against-postgresql"&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Before we could chase down our own bugs, we needed to answer a more basic question: how many of these tests are even valid against PostgreSQL? The suite was originally written for SQLite, and over the years it’s been adapted and extended for MySQL as we’ve used it to test Dolt. Postgres has never been the primary target, so we couldn’t assume the entire test suite would execute cleanly against Postgres.&lt;/p&gt;
&lt;p&gt;We pointed our test runner at a real PostgreSQL server and ran the full suite against stock PostgreSQL first, to establish a baseline of how compatible the tests actually were with Postgres. That baseline surfaced a number of places where the tests themselves (or in many cases, the runner’s expectations about results) were still encoding SQLite or MySQL behavior and not compatible with slightly different behavior in Postgres. Fixing those was a prerequisite before we could start figuring out what changes were needed in Doltgres.&lt;/p&gt;
&lt;p&gt;A few examples of what we found and fixed:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Integer vs. float schema types.&lt;/strong&gt; The test format encodes an expected type for each result column (&lt;code&gt;I&lt;/code&gt; for integer, &lt;code&gt;R&lt;/code&gt; for float/real), based on &lt;a href="https://www.sqlite.org/datatype3.html#type_affinity"&gt;SQLite’s type affinity rules&lt;/a&gt;. Postgres is stricter about numeric types than SQLite, so expressions SQLite treats as integers may legitimately come back as floats from Postgres, and vice versa. We updated the runner’s schema comparison to treat &lt;code&gt;I&lt;/code&gt; and &lt;code&gt;R&lt;/code&gt; as compatible in both directions, and to normalize whole-number floats (like &lt;code&gt;3.000&lt;/code&gt;) to integer formatting (&lt;code&gt;3&lt;/code&gt;) so the value comparison succeeds when the underlying values genuinely match. Limiting this to whole-number floats only means we still detect correctness errors if the values don’t logically match, but we’re more flexible on the returned result type so that we can use the same tests to match against Postgres.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Empty result sets.&lt;/strong&gt; SQLite reports &lt;code&gt;SQLITE_NULL&lt;/code&gt; as the type for every column when a query returns zero rows, which doesn’t correspond to anything meaningful in Postgres. We updated the runner to skip schema-type verification entirely when both the expected and actual result sets are empty, since there’s nothing to compare.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Postgres-specific type names in the harness.&lt;/strong&gt; Our test harness inspects the driver’s reported column types to decide how to parse and compare each value. It was written with MySQL’s type names in mind (&lt;code&gt;INT&lt;/code&gt;, &lt;code&gt;BIGINT&lt;/code&gt;, &lt;code&gt;DECIMAL&lt;/code&gt;, and so on), so it didn’t know what to do with Postgres-specific names like &lt;code&gt;BOOL&lt;/code&gt;, &lt;code&gt;INT2&lt;/code&gt;, or &lt;code&gt;FLOAT4&lt;/code&gt;. We filled in the missing cases so those types get parsed and compared correctly instead of falling through and failing.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;MySQL-only statements.&lt;/strong&gt; Some tests exercise MySQL-specific syntax or behavior that has no Postgres equivalent at all. Rather than force those through, we added skip directives so they’re excluded when running against Doltgres, the same way we already skip SQLite-specific tests.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In addition to those improvements, we also invested in running the suite in parallel, spinning up a single shared Doltgres server and fanning test files out across concurrent workers, each against its own isolated database. At the scale of millions of test queries, that’s the difference between a test run that takes minutes and one that takes hours, which matters a lot when you’re iterating on fixes.&lt;/p&gt;
&lt;h2 id="bugs-the-tests-found-in-doltgres"&gt;Bugs the Tests Found in Doltgres&lt;a class="anchor-link" aria-label="Link to heading" href="#bugs-the-tests-found-in-doltgres"&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;With those changes in place, we could now run the SQL Logic Tests against a real PostgreSQL server and get over 99% correctness. There are still some issues for the test suite to run 100% against PostgreSQL, and we’ll keep chipping away at those in future passes. After these improvements, the remaining test failures with Doltgres were much more likely to be real Doltgres bugs that we needed to dig into. The most interesting was in our &lt;code&gt;COALESCE()&lt;/code&gt; implementation: when called with mixed numeric types (say, an &lt;code&gt;int4&lt;/code&gt; and an &lt;code&gt;int8&lt;/code&gt;, or an &lt;code&gt;int4&lt;/code&gt; and a &lt;code&gt;float8&lt;/code&gt;), it was using a generic type conversion instead of &lt;a href="https://www.postgresql.org/docs/current/typeconv-union-case.html"&gt;Postgres’ assignment cast rules to compute the common type&lt;/a&gt;. That’s an important distinction. Assignment casts are what Postgres itself uses to widen mixed-type arguments to a common type, and using the wrong conversion path meant we could return incorrectly typed or incorrectly rounded results for a fairly common pattern in real SQL.&lt;/p&gt;
&lt;h2 id="on-track-for-august-6th"&gt;On Track for August 6th&lt;a class="anchor-link" aria-label="Link to heading" href="#on-track-for-august-6th"&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Correctly executing 99% of the SQL Logic Test suite was our target for Doltgres’ 1.0 release. It gives us high confidence that a wide range of statements and SQL expressions are executing correctly in Doltgres. By baselining the test suite against PostgreSQL, we discovered that we were closer to this milestone than we initially expected. We thought we still had many remaining gaps to fill to reach that milestone, but it turned out that how the results were being processed by the test runner accounted for most of the gap.&lt;/p&gt;
&lt;p&gt;Executing queries correctly, and returning identical results as PostgreSQL, is the foundation for our 1.0 launch. Without correct query execution, the other goals, like fast execution of queries and tool compatibility, just don’t matter. Overall, we’re making great progress on our 1.0 punch list and remain on track for &lt;strong&gt;August 6th&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;If you’re running Doltgres and you hit a query that returns the wrong result, an error you don’t expect, or behavior that just doesn’t match Postgres, please &lt;a href="https://github.com/dolthub/doltgresql/issues/new"&gt;send us a GitHub issue&lt;/a&gt; and let us know. We want to find and fix as many of these as possible before 1.0 ships, and customer-reported issues go straight to the top of our queue.&lt;/p&gt;
&lt;p&gt;If you haven’t started using Doltgres yet, give it a shot! You can install Doltgres by running &lt;code&gt;brew install doltgres&lt;/code&gt; on a Mac with Homebrew, or you download a binary from our &lt;a href="https://github.com/dolthub/doltgresql/releases"&gt;GitHub releases&lt;/a&gt;. Our dev team hangs out on the DoltHub Discord server every day, so feel free to &lt;a href="https://discord.gg/gqr7K4VNKe"&gt;come by and tell us how it’s going&lt;/a&gt;. We’re closing in on 1.0 and every bit of feedback helps us get there!&lt;/p&gt;</content:encoded>
      <dc:creator>Jason Fulghum</dc:creator>
      <category>doltgres</category>
    </item>
    <item>
      <title>Doltgres 1.0 Coming August 6th</title>
      <link>https://dolthub.com/blog/2026-06-26-doltgres-1-0-coming-this-fall/</link>
      <guid isPermaLink="true">https://dolthub.com/blog/2026-06-26-doltgres-1-0-coming-this-fall/</guid>
      <description>Doltgres 1.0 is coming August 6th. Here's what we've been working on and what we're focused on to get there.</description>
      <pubDate>Fri, 26 Jun 2026 00:00:00 GMT</pubDate>
      <content:encoded>&lt;p&gt;&lt;a href="https://www.doltgres.com/"&gt;Doltgres&lt;/a&gt; is a PostgreSQL-compatible database with Git-style version control built in. It gives you all the power of SQL with the ability to branch, merge, diff, clone, and push your data, using the same model you already know from managing your source code with Git. Doltgres implements the PostgreSQL wire protocol, syntax, and type system so that you can use it just like you use PostgreSQL.&lt;/p&gt;
&lt;p&gt;We first &lt;a href="https://www.dolthub.com/blog/2023-11-01-announcing-doltgresql/"&gt;announced Doltgres in November 2023&lt;/a&gt; as an Alpha, then &lt;a href="https://www.dolthub.com/blog/2025-04-16-doltgres-goes-beta/"&gt;launched the Beta in April 2025&lt;/a&gt;, and now we’re announcing our next major milestone: &lt;strong&gt;Doltgres 1.0, coming August 6th&lt;/strong&gt;. This date is pretty special for us here… it’s the eight-year anniversary of DoltHub! Eight years of hard work leading up to a production-ready, PostgreSQL-compatible versioned database! Not too shabby.&lt;/p&gt;
&lt;p&gt;In this post, we’ll explain what 1.0 means for Doltgres, share what we’re focused on in the next few months, and let you know how you can help.&lt;/p&gt;
&lt;h2 id="what-does-10-mean"&gt;What Does 1.0 Mean?&lt;a class="anchor-link" aria-label="Link to heading" href="#what-does-10-mean"&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Doltgres reaching 1.0 is our signal that Doltgres is ready for production use. That means:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Correctness&lt;/strong&gt; Your queries reliably return the correct results, matching what PostgreSQL returns.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Storage Stability&lt;/strong&gt; The storage format is locked in, and we won’t make breaking storage serialization changes in the 1.x releases that require migrating your data.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Performance&lt;/strong&gt; Query latency is within an acceptable range of PostgreSQL’s performance. We’re shooting to be within 3x PostgreSQL’s latency for key sysbench measurements.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Compatibility&lt;/strong&gt; The tools, libraries, ORMs, and frameworks your team already uses work correctly with Doltgres, too.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Six months ago, we gave an update on Doltgres’ progress in the &lt;a href="https://www.dolthub.com/blog/2025-10-16-state-of-doltgres/"&gt;State of Doltgres&lt;/a&gt;. Since then, we’ve made significant progress towards a 1.0 milestone. Here’s what we’re focused on in the home stretch towards 1.0.&lt;/p&gt;
&lt;h2 id="correctness"&gt;Correctness&lt;a class="anchor-link" aria-label="Link to heading" href="#correctness"&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;One of our key tools for testing SQL correctness is the &lt;a href="https://sqlite.org/sqllogictest/doc/trunk/about.wiki"&gt;SQLite SQL Logic Test suite&lt;/a&gt;. This is a large, open-source test suite originally developed by SQLite containing over seven million test queries covering a wide range of SQL expressions and statements. We’ve forked this module and added many more tests, and we run these against Dolt and Doltgres to measure compatibility and ensure that we’re returning correct results.&lt;/p&gt;
&lt;p&gt;We’ve been tracking our progress on this suite &lt;a href="https://www.dolthub.com/blog/2023-11-27-doltgres-sqllogic-test/"&gt;since the early days of the project&lt;/a&gt; when we were only able to execute 70% of the statements correctly. Today, we’re passing a little over 96% of the tests correctly, and we’re digging into the failing tests to categorize them and tackle the discrepancies.&lt;/p&gt;
&lt;p&gt;Our goal for 1.0 is to reach &lt;strong&gt;99% SQL Logic Test compliance&lt;/strong&gt;. Every percentage point matters here. At this scale of testing, 99% represents millions of individual queries and results that must match PostgreSQL exactly.&lt;/p&gt;
&lt;h2 id="storage-stability"&gt;Storage Stability&lt;a class="anchor-link" aria-label="Link to heading" href="#storage-stability"&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;One thing that makes our major version releases meaningful is the storage format guarantee they carry. For Doltgres 1.0, we’re committing to a stable on-disk serialization format, just like we did for the Dolt 1.0 launch back in 2023. This means &lt;strong&gt;no data migrations will be required&lt;/strong&gt; for any new features we add in the 1.x line of releases. Changing the storage serialization format requires data to be migrated from the old format to the new format, which is disruptive and inconvenient to customers, so we give this guarantee that the storage serialization format won’t change in the 1.x line. When we start to think about a 2.0 release for Doltgres, there may be compelling features or optimizations that require a serialization format change, but we don’t make those changes lightly. There’s a very high bar for backwards incompatible storage serialization changes once we’ve given our customers the green light to use a storage format in production.&lt;/p&gt;
&lt;h2 id="performance"&gt;Performance&lt;a class="anchor-link" aria-label="Link to heading" href="#performance"&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;We’ve been benchmarking Doltgres performance from &lt;a href="https://www.dolthub.com/blog/2023-12-15-benchmarking-postgres-mysql-dolt/"&gt;the beginning&lt;/a&gt;, and we continue to chase down performance bottlenecks as we find them. Because Doltgres uses the same query engine as Dolt (our MySQL-compatible database product), it has access to all the performance improvements we’ve done for Dolt. However, PostgreSQL raises the performance bar from where MySQL set it. In our testing, &lt;a href="https://www.dolthub.com/blog/2024-07-16-mysql-postgres-sysbench-latency/"&gt;we found that PostgreSQL is more than twice as fast as MySQL&lt;/a&gt;, so we’ve been working hard to find more optimizations, do more performance testing, and keep inching closer to PostgreSQL’s performance.&lt;/p&gt;
&lt;p&gt;Our performance goal for 1.0 is to get &lt;a href="https://github.com/akopytov/sysbench"&gt;sysbench&lt;/a&gt; results for Doltgres to within &lt;strong&gt;3x of PostgreSQL&lt;/strong&gt;. We’re currently right on the cusp of achieving this, with a current performance multiplier of 3.3x.&lt;/p&gt;
&lt;p&gt;We continue to find and fix performance issues as they come up. If you encounter a query that is unexpectedly slow, please &lt;a href="https://github.com/dolthub/doltgresql/issues/new"&gt;file an issue&lt;/a&gt; and we’ll dig in and find a way to make it fast for you.&lt;/p&gt;
&lt;h2 id="compatibility"&gt;Compatibility&lt;a class="anchor-link" aria-label="Link to heading" href="#compatibility"&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;PostgreSQL’s ecosystem is vast. Different clients, libraries, ORMs, and tools all speak the PostgreSQL wire protocol in subtly different ways. They use different combinations of the simple and extended query protocols, rely on different parts of &lt;code&gt;pg_catalog&lt;/code&gt;, and have different expectations about types, encodings, and error messages.&lt;/p&gt;
&lt;p&gt;For 1.0, we want to ensure broad compatibility across the PostgreSQL tools most teams are using. This means testing and fixing issues with popular clients and libraries including:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Client libraries&lt;/strong&gt;: psycopg2, asyncpg, pgx, pg (node-postgres), and more&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ORMs&lt;/strong&gt;: Prisma, SQLAlchemy, ActiveRecord, Django ORM, Hibernate, and more&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tools&lt;/strong&gt;: psql, TablePlus, pgAdmin, Dolt Workbench, and more&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Many of these tools, especially the ORMs and higher-level tools make extensive use of the system information tables in &lt;code&gt;pg_catalog&lt;/code&gt; to introspect your schema and display it in a UI, generate migrations, or validate configurations, so this work also includes filling in gaps and fixing incorrect data in those tables so that tools can access the information they need to work correctly with Doltgres.&lt;/p&gt;
&lt;p&gt;We’ve already done a lot of work in this area. We’ve previously written about supporting &lt;a href="https://www.dolthub.com/blog/2026-04-06-doltgresql-prisma/"&gt;Prisma&lt;/a&gt;, &lt;a href="https://www.dolthub.com/blog/2025-06-03-doltgres-laravel/"&gt;Laravel&lt;/a&gt;, &lt;a href="https://www.dolthub.com/blog/2025-04-24-doltgres-django/"&gt;Django&lt;/a&gt;, &lt;a href="https://www.dolthub.com/blog/2025-07-18-sql-alchemy-getting-started-doltgres/"&gt;SQLAlchemy&lt;/a&gt;, and &lt;a href="https://www.dolthub.com/blog/2025-04-21-doltgres-and-knexjs/"&gt;KnexJS&lt;/a&gt;, but there’s more to do. We want to find the issues with popular tools before our customers do. This is another place where your real-world usage can help us find gaps. If you hit any issues with Doltgres and a SQL client library, ORM, or other SQL tool, please &lt;a href="https://github.com/dolthub/doltgresql/issues/new"&gt;report it to us&lt;/a&gt; so we can dig in and fix it.&lt;/p&gt;
&lt;h2 id="other-features"&gt;Other Features&lt;a class="anchor-link" aria-label="Link to heading" href="#other-features"&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Beyond the core themes of correctness, storage stability, performance, and compatibility, there are several features we’re actively developing and vetting for Doltgres’ 1.0 launch:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Remotes and Push/Pull&lt;/strong&gt; &lt;a href="https://www.doltgres.com/docs/concepts/git/remotes/"&gt;Remotes&lt;/a&gt; are a powerful feature that allow you to work in a distributed mode, by pushing data to remotes and pulling data from remotes, just like Git remotes. We’re working to ensure these features are fully supported in Doltgres, enabling the same kinds of remote collaboration and decentralized workflows that Dolt and Git users are familiar with.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Dolt Replication Protocol&lt;/strong&gt; &lt;a href="https://www.doltgres.com/docs/concepts/rdbms/replication/"&gt;Doltgres supports two types of replication&lt;/a&gt;: the PostgreSQL replication protocol and a Dolt-specific replication protocol. The PostgreSQl replication protocol allows you to run DoltgreSQL as a replica of a PostgreSQL server. The Dolt-specific replication protocol allows you to set up more advanced primary/replica configurations and includes features for high-availability and hot-swappable replicas. We’re working to finish testing this support and make sure it’s ready for production.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Garbage Collection Improvements&lt;/strong&gt; As databases accumulate history, they can grow large. Garbage collection helps keeps that growth manageable by cleaning up unreachable data that is no longer needed. Doltgres already supports the standard manually invoking garbage collection with the &lt;code&gt;dolt_gc()&lt;/code&gt; stored procedure, but we don’t want to launch 1.0 without the latest garbage collection improvements: &lt;a href="https://www.dolthub.com/blog/2026-04-28-introducing-incremental-garbage-collection/"&gt;incremental garbage collection&lt;/a&gt; and &lt;a href="https://www.dolthub.com/blog/2025-02-28-announcing-automatic-gc-in-sql-server/"&gt;automatic garbage collection&lt;/a&gt;.&lt;/p&gt;
&lt;h1 id="try-doltgres-and-send-us-issues"&gt;Try Doltgres and Send Us Issues&lt;a class="anchor-link" aria-label="Link to heading" href="#try-doltgres-and-send-us-issues"&gt;#&lt;/a&gt;&lt;/h1&gt;
&lt;p&gt;The best way you can help us get to 1.0 is to &lt;strong&gt;use Doltgres on real workloads and tell us what breaks&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Install Doltgres and try it with your existing PostgreSQL application. Point your ORM at it. Run your migrations. See what works and what doesn’t. Every issue you report makes the product better, and we love to move fast on customer-reported issues.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Install&lt;/strong&gt;: &lt;code&gt;brew install doltgres&lt;/code&gt; or download from our &lt;a href="https://github.com/dolthub/doltgresql/releases"&gt;GitHub releases&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Report issues&lt;/strong&gt;: &lt;a href="https://github.com/dolthub/doltgresql/issues/new"&gt;Create an issue in the Doltgres repository&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;We’re very excited to get Doltgres to 1.0 and announce that it’s ready for production workloads. If you want to be part of getting us there, now is a great time to try out Doltgres! Come by &lt;a href="https://discord.gg/gqr7K4VNKe"&gt;our Discord&lt;/a&gt; and let us know how your experience goes.&lt;/p&gt;</content:encoded>
      <dc:creator>Jason Fulghum</dc:creator>
      <category>doltgres</category>
    </item>
  </channel>
</rss>
