<?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, 09 Oct 2026 03:58:29 GMT</lastBuildDate>
    <atom:link href="https://siftrss.com/f/Kkmm5PG8lw" rel="self" type="application/rss+xml"/>
    <item>
      <title>Finding Bugs Using Join Implication Reasoning</title>
      <link>https://dolthub.com/blog/2026-10-05-join-implication-reasoning/</link>
      <guid isPermaLink="true">https://dolthub.com/blog/2026-10-05-join-implication-reasoning/</guid>
      <description>We review "Detecting Join Bugs in Database Engines via Join Implication Reasoning," a research paper from the TEST Lab at NUS that tests the correctness of join queries in databases by generating equivalent join queries.</description>
      <pubDate>Mon, 05 Oct 2026 00:00:00 GMT</pubDate>
      <content:encoded>&lt;p&gt;As the world’s first version-controlled SQL database and with &lt;a href="https://github.com/dolthub/dolt"&gt;24.6k (and counting) stars on GitHub&lt;/a&gt;, Dolt has received a lot of interest from academic researchers over the years. In the past couple of weeks, I have been writing about the &lt;a href="https://www.dolthub.com/blog/2026-09-18-nus-test-lab/"&gt;research with Dolt done by the TEST Lab at National University of Singapore&lt;/a&gt;. Last week, I wrote about Suyang Zhong’s work on &lt;a href="https://www.dolthub.com/blog/2026-09-25-sqlancerplusplus/"&gt;SQLancer++&lt;/a&gt;. Today, I will be reviewing &lt;a href="https://dl.acm.org/doi/10.1145/3802036"&gt;“Detecting Join Bugs in Database Engines via Join Implication Reasoning”&lt;/a&gt; by Zhaokun Xiang. This paper was published earlier this year as part of &lt;a href="https://dl.acm.org/toc/pacmmod/2026/4/3"&gt;Proceedings of the ACM on Management of Data&lt;/a&gt; and presented at &lt;a href="https://sigmod.org/"&gt;SIGMOD&lt;/a&gt;. Some of y’all may know Zhaokun Xiang as &lt;a href="https://github.com/TheoristCoder"&gt;&lt;code&gt;theoristcoder&lt;/code&gt; on GitHub&lt;/a&gt; – last year, I wrote about the &lt;a href="https://www.dolthub.com/blog/2025-12-03-new-zero-time/"&gt;changes made to zero time&lt;/a&gt; due to a bug found by Zhaokun. Now, let’s dive into the paper.&lt;/p&gt;
&lt;h1 id="join-implication-reasoning"&gt;Join Implication Reasoning&lt;a class="anchor-link" aria-label="Link to heading" href="#join-implication-reasoning"&gt;#&lt;/a&gt;&lt;/h1&gt;
&lt;p&gt;One of the biggest reasons to choose relational databases over their non-relational counterparts is the support for complex &lt;a href="https://en.wikipedia.org/wiki/Join_(SQL)"&gt;joins&lt;/a&gt;. In order to execute a join query as efficiently as possible, SQL query analyzers apply a series of optimization rules, including &lt;a href="https://www.dolthub.com/blog/2020-10-28-pushdown-filters/"&gt;filter pushdowns&lt;/a&gt;, &lt;a href="https://www.dolthub.com/blog/2023-12-13-functional-dependency-analysis/"&gt;join reordering&lt;/a&gt;, &lt;a href="https://www.dolthub.com/blog/2022-11-04-memoizing-joins/"&gt;memoization&lt;/a&gt;, and &lt;a href="https://www.dolthub.com/blog/2024-01-22-join-statistics/"&gt;cost analysis&lt;/a&gt;. Many of these optimization rules have been implemented in &lt;a href="https://github.com/dolthub/go-mysql-server/tree/main"&gt;go-mysql-server&lt;/a&gt;, the SQL query engine that powers Dolt. However, fast doesn’t always mean correct if the rules are not applied properly – after all, &lt;a href="https://www.youtube.com/watch?v=b2F-DItXtZs"&gt;piping to &lt;code&gt;/dev/null&lt;/code&gt; is fast as hell&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Join implication reasoning (JIR) is a testing technique implemented as an extension of &lt;a href="https://sqlancer.github.io/"&gt;SQLancer&lt;/a&gt; that focuses on join correctness. Oftentimes, a join query can be rewritten to use different join types and still produce the same result – this is the motivation for join reordering during query optimization. JIR takes these logically equivalent join queries and uses them as test oracles, making sure that a database actually returns the same result for both queries. The paper outlines six join-equivalence rules as well as proofs for each rule. For example, a left outer join can be rewritten as a combination of an inner join and an anti-join with appropriate null-padding. Another example is that a right outer join is the same as a left outer join with the sides of the join swapped and vice versa.&lt;/p&gt;
&lt;p&gt;The implementation of JIR involves generating random join queries represented by &lt;a href="https://en.wikipedia.org/wiki/Abstract_syntax_tree"&gt;abstract syntax trees (ASTs)&lt;/a&gt; and randomly generated boolean join predicates (also known as join conditions). More queries are subsequently generated by replacing the join types at each node of the ASTs. Equivalent queries are generated as well using the join-equivalence rules. Each pair of equivalent queries is run against the database and the results are compared.&lt;/p&gt;
&lt;p&gt;JIR was implemented in both SQLancer and SQLancer++. While SQLancer was already able to generate joins, JIR extended it by generating joins with subqueries and adding randomly generated join conditions.&lt;/p&gt;
&lt;p&gt;To compare the efficacy of JIR, databases were run with both JIR and &lt;a href="https://dl.acm.org/doi/10.1145/3654991"&gt;differential query plan (DQP)&lt;/a&gt; extensions of SQLancer or SQLancer++, and the number of bugs found by each method were compared to each other. DQP is an earlier project from the TEST Lab which was published in 2024.&lt;/p&gt;
&lt;h2 id="so-does-it-use-ai"&gt;So does it use AI?&lt;a class="anchor-link" aria-label="Link to heading" href="#so-does-it-use-ai"&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Similar to SQLancer++, JIR uses rule-based generation and does not use a &lt;a href="https://en.wikipedia.org/wiki/Large_language_model"&gt;large language model (LLM)&lt;/a&gt; or &lt;a href="https://en.wikipedia.org/wiki/Generative_AI#Generative_neural_networks_(since_the_late_2000s)"&gt;generative neural network&lt;/a&gt;, so it does not use “generative AI” in the 2026 colloquial sense of the term. Someone 20 years ago might call it AI, but it’s unlikely that someone today would.&lt;/p&gt;
&lt;h1 id="impact-on-dolt"&gt;Impact on Dolt&lt;a class="anchor-link" aria-label="Link to heading" href="#impact-on-dolt"&gt;#&lt;/a&gt;&lt;/h1&gt;
&lt;p&gt;The paper claims to have found 12 unique bugs in Dolt via JIR and 3 unique bugs via DQP; at the time the paper was written, 10 out of 12 of the JIR-found bugs were fixed and all of the DQP-found bugs were fixed. From my perspective, I’m not able to identify via our issues queue whether a bug was found by JIR or DQP and what criterion for uniqueness was used, but Zhaokun did file &lt;a href="https://github.com/dolthub/dolt/issues?q=is%3Aissue+author%3Atheoristcoder"&gt;19 bugs&lt;/a&gt; between November 2025 and February 2026. As of February 10, 2026, every single bug has been fixed.&lt;/p&gt;
&lt;p&gt;The majority of these bugs involved anti-joins. This makes sense since anti-joins often involve subqueries, which were previously not supported for join queries in SQLancer. There were also several bugs involving left joins and a few bugs involving how inner joins were optimized into other join types during join planning.&lt;/p&gt;
&lt;p&gt;Correctness in Dolt is something we really value, and we are really appreciative of the work Zhaokun has done to ensure the correctness in joins. As mentioned in the paper, join bugs can be tricky to identify due to the number of optimization rules applied and they can get especially complex when multiple joins are involved.&lt;/p&gt;
&lt;p&gt;Are you an academic researcher working on a project with Dolt or another DoltHub database? Join our &lt;a href="https://discord.gg/b3deB8WJh"&gt;Discord community&lt;/a&gt; – we’d love to hear from you.&lt;/p&gt;</content:encoded>
      <dc:creator>Angela Xie</dc:creator>
      <category>dolt</category>
    </item>
    <item>
      <title>Your Replica Doesn't Need to Know Everything</title>
      <link>https://dolthub.com/blog/2026-09-23-wildcard-binlog-filters/</link>
      <guid isPermaLink="true">https://dolthub.com/blog/2026-09-23-wildcard-binlog-filters/</guid>
      <description>Dolt now supports MySQL-compatible wildcard table filters for binlog replication, so replicas can selectively apply row changes from matching tables.</description>
      <pubDate>Wed, 23 Sep 2026 00:00:00 GMT</pubDate>
      <content:encoded>&lt;p&gt;&lt;a href="https://doltdb.com/"&gt;Dolt&lt;/a&gt; is a MySQL-compatible SQL database with Git-style version control built in. You can branch, merge, diff, clone, push, and pull your data just like you do with source code. You can also connect Dolt to MySQL’s binlog replication protocol, which opens up several useful architectures between MySQL (or MariaDB) and Dolt.&lt;/p&gt;
&lt;p&gt;Replicas tend to be enthusiastic little copy machines. Unless told otherwise, they copy every change they receive. That is exactly what you want for a hot standby, but it can be wasteful when a replica serves a narrower purpose and only needs changes from a particular family of tables.&lt;/p&gt;
&lt;p&gt;A Dolt user recently &lt;a href="https://github.com/dolthub/dolt/issues/11787"&gt;opened a feature request&lt;/a&gt; asking us to support MySQL’s &lt;code&gt;REPLICATE_WILD_DO_TABLE&lt;/code&gt; and &lt;code&gt;REPLICATE_WILD_IGNORE_TABLE&lt;/code&gt; filters. It was a useful, well-scoped request, and we were happy to turn around the wildcard filtering support in less than a week. It shipped in &lt;a href="https://github.com/dolthub/dolt/releases/tag/v2.3.5"&gt;Dolt 2.3.5&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Let’s look at why you might want a filtered replica and then investigate a string of suspicious lunch disappearances. This investigation may be more rigorous than the situation warrants, but that has never stopped a database engineer before.&lt;/p&gt;
&lt;h2 id="why-filter-a-replica"&gt;Why Filter a Replica?&lt;a class="anchor-link" aria-label="Link to heading" href="#why-filter-a-replica"&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Traditional replication use cases usually need a complete copy of the source database:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;high-availability replica&lt;/strong&gt; needs all the data so it can take over if the primary fails.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;disaster-recovery replica&lt;/strong&gt; needs a complete copy in another availability zone or region.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;read-scaling replica&lt;/strong&gt; often serves the same application queries as the primary.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Filtered replicas solve a different problem. An analytics replica may only need &lt;code&gt;orders_*&lt;/code&gt; and &lt;code&gt;sales_*&lt;/code&gt; tables. A service-specific read model may only need tables owned by one application domain. A regional replica may only need tables named for that region. You may also want to exclude high-volume scratch or archive tables that provide no value on the replica.&lt;/p&gt;
&lt;p&gt;One of my favorite uses of Dolt’s binlog replication support is adding a Dolt replica to an existing MySQL or MariaDB system. The application keeps using its current primary while Dolt receives selected changes and turns them into versioned Dolt commits. This gives you a queryable history, including diffs between revisions, without requiring a full database migration on day one.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.dolthub.com/blogimages/wildcard-binlog-filtering.svg/a3fb00a1c9a42c61284d7ffde4f885acbcc7365c6e3fd81036a29dca9d12029d.svg" alt="Wildcard row filters send matching row events to a versioned Dolt replica while nonmatching row events are skipped"&gt;&lt;/p&gt;
&lt;h2 id="wildcard-table-filters"&gt;Wildcard Table Filters&lt;a class="anchor-link" aria-label="Link to heading" href="#wildcard-table-filters"&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Dolt already supported exact table filters through &lt;code&gt;REPLICATE_DO_TABLE&lt;/code&gt; and &lt;code&gt;REPLICATE_IGNORE_TABLE&lt;/code&gt;. Dolt 2.3.5 adds the wildcard variants:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;REPLICATE_WILD_DO_TABLE&lt;/code&gt; replicates row changes from tables that match a pattern and excludes unmatched tables.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;REPLICATE_WILD_IGNORE_TABLE&lt;/code&gt; replicates row changes normally but skips tables that match a pattern.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Patterns contain a database pattern and a table pattern separated by a period. They use MySQL’s wildcard syntax:&lt;/p&gt;

























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Pattern&lt;/th&gt;&lt;th&gt;Meaning&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;%&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Match any sequence of bytes&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;_&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Match one byte&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;\%&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Match a literal percent sign&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;\_&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Match a literal underscore&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;For example, &lt;code&gt;sales.orders%&lt;/code&gt; matches tables in the &lt;code&gt;sales&lt;/code&gt; database whose names start with &lt;code&gt;orders&lt;/code&gt;. Because &lt;code&gt;_&lt;/code&gt; is a wildcard, a literal underscore must be escaped. The pattern &lt;code&gt;breakroom.case\_%&lt;/code&gt; matches &lt;code&gt;case_open&lt;/code&gt; and &lt;code&gt;case_closed&lt;/code&gt;, but not &lt;code&gt;caseworker_notes&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id="the-great-office-lunch-investigation"&gt;The Great Office Lunch Investigation&lt;a class="anchor-link" aria-label="Link to heading" href="#the-great-office-lunch-investigation"&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;For this demo, we have a MySQL source server and a Dolt replica. The MySQL server is running on port &lt;code&gt;11229&lt;/code&gt; with GTIDs, binary logging, and row-format binlogs enabled. If you already have a MySQL source configured, you can skip the setup below, but keep in mind that you’ll need to update the replica configuration below with your existing server’s details. If your source contains existing data, our &lt;a href="https://www.dolthub.com/docs/guides/binlog-replication/"&gt;versioned MySQL replica guide&lt;/a&gt; also explains how to warm the replica with a snapshot before starting replication.&lt;/p&gt;
&lt;h3 id="start-a-local-mysql-source"&gt;Start a Local MySQL Source&lt;a class="anchor-link" aria-label="Link to heading" href="#start-a-local-mysql-source"&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;First, create an isolated MySQL data directory. &lt;code&gt;--initialize-insecure&lt;/code&gt; gives the local &lt;code&gt;root&lt;/code&gt; account no password, which is convenient for this disposable demo and a terrible production security policy.&lt;/p&gt;
&lt;pre class="astro-code github-dark" tabindex="0" data-language="sh"&gt;&lt;code&gt;&lt;span class="line"&gt;&lt;span&gt;mkdir&lt;/span&gt;&lt;span&gt; mysql-source&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;cd&lt;/span&gt;&lt;span&gt; mysql-source&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;mysqld&lt;/span&gt;&lt;span&gt; --no-defaults&lt;/span&gt;&lt;span&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;  --initialize-insecure&lt;/span&gt;&lt;span&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;  --datadir=&lt;/span&gt;&lt;span&gt;"&lt;/span&gt;&lt;span&gt;$PWD&lt;/span&gt;&lt;span&gt;/data"&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now start MySQL with binary logging, row-format events, GTIDs, and a unique server ID:&lt;/p&gt;
&lt;pre class="astro-code github-dark" tabindex="0" data-language="sh"&gt;&lt;code&gt;&lt;span class="line"&gt;&lt;span&gt;mysqld&lt;/span&gt;&lt;span&gt; --no-defaults&lt;/span&gt;&lt;span&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;  --datadir=&lt;/span&gt;&lt;span&gt;"&lt;/span&gt;&lt;span&gt;$PWD&lt;/span&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;  --port=11229&lt;/span&gt;&lt;span&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;  --bind-address=127.0.0.1&lt;/span&gt;&lt;span&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;  --socket=&lt;/span&gt;&lt;span&gt;"&lt;/span&gt;&lt;span&gt;$PWD&lt;/span&gt;&lt;span&gt;/mysql.sock"&lt;/span&gt;&lt;span&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;  --server-id=41&lt;/span&gt;&lt;span&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;  --log-bin=&lt;/span&gt;&lt;span&gt;"&lt;/span&gt;&lt;span&gt;$PWD&lt;/span&gt;&lt;span&gt;/mysql-bin"&lt;/span&gt;&lt;span&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;  --binlog-format=ROW&lt;/span&gt;&lt;span&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;  --gtid-mode=ON&lt;/span&gt;&lt;span&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;  --enforce-gtid-consistency=ON&lt;/span&gt;&lt;span&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;  --mysqlx=0&lt;/span&gt;&lt;span&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;  --pid-file=&lt;/span&gt;&lt;span&gt;"&lt;/span&gt;&lt;span&gt;$PWD&lt;/span&gt;&lt;span&gt;/mysql.pid"&lt;/span&gt;&lt;span&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;  --log-error=&lt;/span&gt;&lt;span&gt;"&lt;/span&gt;&lt;span&gt;$PWD&lt;/span&gt;&lt;span&gt;/mysql.err"&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The four settings that matter to replication are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;server-id=41&lt;/code&gt; uniquely identifies this source in the replication topology.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;log-bin&lt;/code&gt; enables the binary log that replicas consume.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;binlog-format=ROW&lt;/code&gt; records the changed rows instead of replaying DML statements.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;gtid-mode&lt;/code&gt; and &lt;code&gt;enforce-gtid-consistency&lt;/code&gt; enable GTID auto-positioning.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Leave that terminal running. In another terminal, connect to the source over TCP:&lt;/p&gt;
&lt;pre class="astro-code github-dark" tabindex="0" data-language="sh"&gt;&lt;code&gt;&lt;span class="line"&gt;&lt;span&gt;mysql&lt;/span&gt;&lt;span&gt; --protocol&lt;/span&gt;&lt;span&gt; TCP&lt;/span&gt;&lt;span&gt; -h&lt;/span&gt;&lt;span&gt; 127.0.0.1&lt;/span&gt;&lt;span&gt; -P&lt;/span&gt;&lt;span&gt; 11229&lt;/span&gt;&lt;span&gt; -uroot&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can verify the important settings from the MySQL shell:&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;SHOW VARIABLES&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;WHERE&lt;/span&gt;&lt;span&gt; Variable_Name &lt;/span&gt;&lt;span&gt;IN&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;  (&lt;/span&gt;&lt;span&gt;'log_bin'&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;'binlog_format'&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;'enforce_gtid_consistency'&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;'gtid_mode'&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;'server_id'&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;| Variable_name            | &lt;/span&gt;&lt;span&gt;Value&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;| binlog_format            | &lt;/span&gt;&lt;span&gt;ROW&lt;/span&gt;&lt;span&gt;   |&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;| enforce_gtid_consistency | &lt;/span&gt;&lt;span&gt;ON&lt;/span&gt;&lt;span&gt;    |&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;| gtid_mode                | &lt;/span&gt;&lt;span&gt;ON&lt;/span&gt;&lt;span&gt;    |&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;| log_bin                  | &lt;/span&gt;&lt;span&gt;ON&lt;/span&gt;&lt;span&gt;    |&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;| server_id                | &lt;/span&gt;&lt;span&gt;41&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;From a MySQL shell connected to the source, create a dedicated replication user:&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; USER&lt;/span&gt;&lt;span&gt; '&lt;/span&gt;&lt;span&gt;replicator&lt;/span&gt;&lt;span&gt;'@&lt;/span&gt;&lt;span&gt;'%'&lt;/span&gt;&lt;span&gt; IDENTIFIED &lt;/span&gt;&lt;span&gt;BY&lt;/span&gt;&lt;span&gt; 'password'&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;GRANT&lt;/span&gt;&lt;span&gt; REPLICATION SLAVE &lt;/span&gt;&lt;span&gt;ON&lt;/span&gt;&lt;span&gt; *&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;span&gt; TO&lt;/span&gt;&lt;span&gt; 'replicator'&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;h3 id="start-the-dolt-replica"&gt;Start the Dolt Replica&lt;a class="anchor-link" aria-label="Link to heading" href="#start-the-dolt-replica"&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Next, start a fresh Dolt SQL server for the replica. This demo requires Dolt 2.3.5 or later.&lt;/p&gt;
&lt;pre class="astro-code github-dark" tabindex="0" data-language="sh"&gt;&lt;code&gt;&lt;span class="line"&gt;&lt;span&gt;mkdir&lt;/span&gt;&lt;span&gt; dolt-replica&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;cd&lt;/span&gt;&lt;span&gt; dolt-replica&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;dolt&lt;/span&gt;&lt;span&gt; init&lt;/span&gt;&lt;span&gt; --name&lt;/span&gt;&lt;span&gt; "Lunch Detective"&lt;/span&gt;&lt;span&gt; --email&lt;/span&gt;&lt;span&gt; "detective@example.com"&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;dolt&lt;/span&gt;&lt;span&gt; sql-server&lt;/span&gt;&lt;span&gt; --port&lt;/span&gt;&lt;span&gt; 11230&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In another terminal, connect to the Dolt server with the MySQL client:&lt;/p&gt;
&lt;pre class="astro-code github-dark" tabindex="0" data-language="sh"&gt;&lt;code&gt;&lt;span class="line"&gt;&lt;span&gt;mysql&lt;/span&gt;&lt;span&gt; --protocol&lt;/span&gt;&lt;span&gt; TCP&lt;/span&gt;&lt;span&gt; -h&lt;/span&gt;&lt;span&gt; 127.0.0.1&lt;/span&gt;&lt;span&gt; -P&lt;/span&gt;&lt;span&gt; 11230&lt;/span&gt;&lt;span&gt; -uroot&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Configure the MySQL source, install our wildcard filter, and start replication:&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;SET&lt;/span&gt;&lt;span&gt; @@&lt;/span&gt;&lt;span&gt;GLOBAL&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;server_id&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; 42&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;CHANGE REPLICATION SOURCE &lt;/span&gt;&lt;span&gt;TO&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;  SOURCE_HOST &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; '127.0.0.1'&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;  SOURCE_PORT &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; 11229&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;  SOURCE_USER &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; 'replicator'&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;  SOURCE_PASSWORD &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; 'password'&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;  SOURCE_AUTO_POSITION &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 class="line"&gt;&lt;span&gt;CHANGE REPLICATION &lt;/span&gt;&lt;span&gt;FILTER&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;  REPLICATE_WILD_DO_TABLE &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;'breakroom.case\\_%'&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;START&lt;/span&gt;&lt;span&gt; REPLICA&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;One restart note: &lt;a href="https://dev.mysql.com/doc/refman/8.4/en/change-replication-filter.html"&gt;MySQL does not persist filters set with &lt;code&gt;CHANGE REPLICATION FILTER&lt;/code&gt;&lt;/a&gt;, and Dolt matches that behavior. The connection settings from &lt;code&gt;CHANGE REPLICATION SOURCE&lt;/code&gt; persist, but you must apply the wildcard filter again after restarting the Dolt server. For a planned restart, run &lt;code&gt;STOP REPLICA&lt;/code&gt; before shutting down, then reapply the filter before running &lt;code&gt;START REPLICA&lt;/code&gt; again. This avoids briefly auto-starting replication without the filter in place.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;There are two backslashes in the SQL string because one escapes the other in the string literal. The resulting filter pattern contains one backslash, which tells the wildcard matcher to treat the underscore literally.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;CHANGE REPLICATION FILTER&lt;/code&gt; cannot run while the replica SQL thread is active. If you are changing filters on an existing replica, run &lt;code&gt;STOP REPLICA&lt;/code&gt; first, update the filters, and then run &lt;code&gt;START REPLICA&lt;/code&gt; again.&lt;/p&gt;
&lt;p&gt;We can confirm that both replication and the filter are active:&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;SHOW &lt;/span&gt;&lt;span&gt;REPLICA&lt;/span&gt;&lt;span&gt; STATUS&lt;/span&gt;&lt;span&gt;\G&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The complete output contains more replication state, but these are the fields we care about:&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;            Replica_IO_Running: Yes&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;           Replica_SQL_Running: Yes&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;       Replicate_Wild_Do_Table: breakroom.case\_%&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;   Replicate_Wild_Ignore_Table:&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now reconnect to the MySQL source and create the world’s most over-engineered breakroom incident-response system:&lt;/p&gt;
&lt;pre class="astro-code github-dark" tabindex="0" data-language="sh"&gt;&lt;code&gt;&lt;span class="line"&gt;&lt;span&gt;mysql&lt;/span&gt;&lt;span&gt; --protocol&lt;/span&gt;&lt;span&gt; TCP&lt;/span&gt;&lt;span&gt; -h&lt;/span&gt;&lt;span&gt; 127.0.0.1&lt;/span&gt;&lt;span&gt; -P&lt;/span&gt;&lt;span&gt; 11229&lt;/span&gt;&lt;span&gt; -uroot&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&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; breakroom&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; breakroom;&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&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; case_open&lt;/span&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;INT&lt;/span&gt;&lt;span&gt; PRIMARY KEY&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;  missing_lunch &lt;/span&gt;&lt;span&gt;VARCHAR&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;  prime_suspect &lt;/span&gt;&lt;span&gt;VARCHAR&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;);&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&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; case_closed&lt;/span&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;INT&lt;/span&gt;&lt;span&gt; PRIMARY KEY&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;  missing_lunch &lt;/span&gt;&lt;span&gt;VARCHAR&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;  resolution &lt;/span&gt;&lt;span&gt;VARCHAR&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;);&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&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; snack_inventory&lt;/span&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;INT&lt;/span&gt;&lt;span&gt; PRIMARY KEY&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;  snack &lt;/span&gt;&lt;span&gt;VARCHAR&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;  quantity &lt;/span&gt;&lt;span&gt;INT&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;CREATE&lt;/span&gt;&lt;span&gt; TABLE&lt;/span&gt;&lt;span&gt; employee_alibis&lt;/span&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;INT&lt;/span&gt;&lt;span&gt; PRIMARY KEY&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;  employee &lt;/span&gt;&lt;span&gt;VARCHAR&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;  alibi &lt;/span&gt;&lt;span&gt;VARCHAR&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;200&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;Next, add a little evidence:&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;INSERT INTO&lt;/span&gt;&lt;span&gt; case_open &lt;/span&gt;&lt;span&gt;VALUES&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;  (&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;'Greek yogurt'&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;'The new intern'&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;2&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;'Leftover pizza'&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;'Anyone working past 6 PM'&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;INSERT INTO&lt;/span&gt;&lt;span&gt; case_closed &lt;/span&gt;&lt;span&gt;VALUES&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;  (&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;'Turkey sandwich'&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;'Owner ate it and forgot'&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;INSERT INTO&lt;/span&gt;&lt;span&gt; snack_inventory &lt;/span&gt;&lt;span&gt;VALUES&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;  (&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;'Sea salt chips'&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;14&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;2&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;'Emergency chocolate'&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;INSERT INTO&lt;/span&gt;&lt;span&gt; employee_alibis &lt;/span&gt;&lt;span&gt;VALUES&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;  (&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;'Zach'&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;'In a meeting with twelve witnesses'&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;2&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;'Neil'&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;'Claims he has never seen a sandwich'&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Back on the Dolt replica, all four tables exist:&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;USE&lt;/span&gt;&lt;span&gt; breakroom;&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;SHOW TABLES;&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;| Tables_in_breakroom |&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;| case_closed         |&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;| case_open           |&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;| employee_alibis     |&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;| snack_inventory     |&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;This is an important detail: replication table filters apply to row events. MySQL sends DDL such as &lt;code&gt;CREATE TABLE&lt;/code&gt; in query events, so DDL is not filtered. The replica knows that all four tables exist; it has merely been instructed not to gossip about all of their rows.&lt;/p&gt;
&lt;p&gt;Let’s count the rows:&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; 'case_open'&lt;/span&gt;&lt;span&gt; AS&lt;/span&gt;&lt;span&gt; table_name, &lt;/span&gt;&lt;span&gt;COUNT&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;AS&lt;/span&gt;&lt;span&gt; replicated_rows &lt;/span&gt;&lt;span&gt;FROM&lt;/span&gt;&lt;span&gt; case_open&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;UNION ALL&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;SELECT&lt;/span&gt;&lt;span&gt; 'case_closed'&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;*&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;FROM&lt;/span&gt;&lt;span&gt; case_closed&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;UNION ALL&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;SELECT&lt;/span&gt;&lt;span&gt; 'snack_inventory'&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;*&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;FROM&lt;/span&gt;&lt;span&gt; snack_inventory&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;UNION ALL&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;SELECT&lt;/span&gt;&lt;span&gt; 'employee_alibis'&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;*&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;FROM&lt;/span&gt;&lt;span&gt; employee_alibis;&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;| table_name      | replicated_rows |&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;| case_open       |               &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;| case_closed     |               &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;| snack_inventory |               &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;| employee_alibis |               &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;The wildcard filter admitted row changes for &lt;code&gt;case_open&lt;/code&gt; and &lt;code&gt;case_closed&lt;/code&gt; and skipped the other two tables. Neil’s alibi has been filtered out, which may be for the best.&lt;/p&gt;
&lt;h2 id="dolt-version-control"&gt;Dolt Version Control&lt;a class="anchor-link" aria-label="Link to heading" href="#dolt-version-control"&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Because the replica is Dolt, the replicated transactions also create versioned history. By default, Dolt creates a new commit as each source transaction is applied, and the commit message records the source GTID. We can see the latest two replicated transactions in &lt;code&gt;dolt_log&lt;/code&gt;:&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; LEFT&lt;/span&gt;&lt;span&gt;(commit_hash, &lt;/span&gt;&lt;span&gt;8&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;AS&lt;/span&gt;&lt;span&gt; commit_hash, &lt;/span&gt;&lt;span&gt;message&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;FROM&lt;/span&gt;&lt;span&gt; dolt_log&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;LIMIT&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;+&lt;/span&gt;&lt;span&gt;-------------+-------------------------------------------------------------------------+&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;| commit_hash | &lt;/span&gt;&lt;span&gt;message&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;| dp5f0jhp    | Dolt binlog &lt;/span&gt;&lt;span&gt;replica&lt;/span&gt;&lt;span&gt; commit&lt;/span&gt;&lt;span&gt;: GTID 614fa9ba&lt;/span&gt;&lt;span&gt;-&lt;/span&gt;&lt;span&gt;b5e2&lt;/span&gt;&lt;span&gt;-&lt;/span&gt;&lt;span&gt;11f1&lt;/span&gt;&lt;span&gt;-&lt;/span&gt;&lt;span&gt;9b3e&lt;/span&gt;&lt;span&gt;-&lt;/span&gt;&lt;span&gt;274e157c3285:&lt;/span&gt;&lt;span&gt;9&lt;/span&gt;&lt;span&gt; |&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;| 47e7htpp    | Dolt binlog &lt;/span&gt;&lt;span&gt;replica&lt;/span&gt;&lt;span&gt; commit&lt;/span&gt;&lt;span&gt;: GTID 614fa9ba&lt;/span&gt;&lt;span&gt;-&lt;/span&gt;&lt;span&gt;b5e2&lt;/span&gt;&lt;span&gt;-&lt;/span&gt;&lt;span&gt;11f1&lt;/span&gt;&lt;span&gt;-&lt;/span&gt;&lt;span&gt;9b3e&lt;/span&gt;&lt;span&gt;-&lt;/span&gt;&lt;span&gt;274e157c3285:&lt;/span&gt;&lt;span&gt;8&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;Your commit hashes and source UUID will be different, but the messages will identify the source transaction behind each commit. That history is queryable data, not a stack of opaque binlog files.&lt;/p&gt;
&lt;p&gt;When investigating a change, we can start with &lt;code&gt;dolt_diff_stat()&lt;/code&gt; to see which tables changed between two commits and how many rows were added, deleted, or modified. Here we compare the latest commit with its parent:&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; table_name, rows_added, rows_deleted, rows_modified&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;FROM&lt;/span&gt;&lt;span&gt; dolt_diff_stat(&lt;/span&gt;&lt;span&gt;'HEAD~'&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;'HEAD'&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;| table_name  | rows_added | rows_deleted | rows_modified |&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;| case_closed |          &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;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;The stats point us to &lt;code&gt;case_closed&lt;/code&gt;, where one row was added. Now we can use &lt;code&gt;dolt_diff()&lt;/code&gt; with the same two commits to see the exact change:&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; diff_type, to_id, to_missing_lunch, to_resolution&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;FROM&lt;/span&gt;&lt;span&gt; dolt_diff(&lt;/span&gt;&lt;span&gt;'HEAD~'&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;'HEAD'&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;'case_closed'&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;| diff_type | to_id | to_missing_lunch | to_resolution           |&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;| added     |     &lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt; | Turkey sandwich  | &lt;/span&gt;&lt;span&gt;Owner&lt;/span&gt;&lt;span&gt; ate it &lt;/span&gt;&lt;span&gt;and&lt;/span&gt;&lt;span&gt; forgot |&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;The lunch thief remains at large, but now every case update leaves a versioned trail. We may not have a culprit yet, but sooner or later, somebody is going to get caught bread-handed.&lt;/p&gt;
&lt;h2 id="choosing-the-right-filter"&gt;Choosing the Right Filter&lt;a class="anchor-link" aria-label="Link to heading" href="#choosing-the-right-filter"&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Use a wildcard &lt;code&gt;DO&lt;/code&gt; filter when the replica should receive only a named family of tables:&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;CHANGE REPLICATION &lt;/span&gt;&lt;span&gt;FILTER&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;  REPLICATE_WILD_DO_TABLE &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;'sales.orders%'&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Use a wildcard &lt;code&gt;IGNORE&lt;/code&gt; filter when the replica should receive everything except a named family:&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;CHANGE REPLICATION &lt;/span&gt;&lt;span&gt;FILTER&lt;/span&gt;&lt;/span&gt;
&lt;span class="line"&gt;&lt;span&gt;  REPLICATE_WILD_IGNORE_TABLE &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;'sales.scratch%'&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Be careful when combining filter types. MySQL-compatible table-filter precedence is:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;An exact &lt;code&gt;DO&lt;/code&gt; match replicates the row.&lt;/li&gt;
&lt;li&gt;An exact &lt;code&gt;IGNORE&lt;/code&gt; match skips it.&lt;/li&gt;
&lt;li&gt;A wildcard &lt;code&gt;DO&lt;/code&gt; match replicates it.&lt;/li&gt;
&lt;li&gt;A wildcard &lt;code&gt;IGNORE&lt;/code&gt; match skips it.&lt;/li&gt;
&lt;li&gt;If any &lt;code&gt;DO&lt;/code&gt; rules exist, an unmatched row is skipped; otherwise it is replicated.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The early match wins. In particular, a broad wildcard &lt;code&gt;DO&lt;/code&gt; pattern cannot be narrowed with an overlapping wildcard &lt;code&gt;IGNORE&lt;/code&gt; pattern, because the wildcard &lt;code&gt;DO&lt;/code&gt; rule is checked first. If you want everything except &lt;code&gt;scratch%&lt;/code&gt;, use the wildcard &lt;code&gt;IGNORE&lt;/code&gt; rule by itself.&lt;/p&gt;
&lt;p&gt;One other operational detail worth remembering: filters affect new row events; they do not remove data already present on the replica, so an initial snapshot must be scoped appropriately.&lt;/p&gt;
&lt;h2 id="wrapping-up"&gt;Wrapping Up&lt;a class="anchor-link" aria-label="Link to heading" href="#wrapping-up"&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Wildcard binlog filters make it easier to build focused Dolt replicas for analytics, audit history, staged migrations, and service-specific read workloads. You can select whole families of tables without maintaining an ever-growing list of exact table names, while the replicated data still gets the benefits of Dolt’s versioned history.&lt;/p&gt;
&lt;p&gt;This feature exists because a user told us about a gap that mattered in their deployment. We love getting reports like that. If you hit a missing feature, compatibility gap, or bug, please &lt;a href="https://github.com/dolthub/dolt/issues/new"&gt;open an issue on GitHub&lt;/a&gt; or &lt;a href="https://discord.gg/gqr7K4VNKe"&gt;come talk to us on Discord&lt;/a&gt;. User bug reports and feature requests help us understand what matters to real systems, and sometimes we can get a fix into your hands before your lunch disappears again.&lt;/p&gt;</content:encoded>
      <dc:creator>Jason Fulghum</dc:creator>
      <category>feature release</category>
      <category>dolt</category>
    </item>
    <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 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 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 definitions 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 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 definitions 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 definitions themselves can become quite verbose. And every function that accepts these types must duplicate the type constraint. 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>
  </channel>
</rss>
