<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: alexey.zh</title>
    <description>The latest articles on DEV Community by alexey.zh (@alzhi_f93e67fa45b972).</description>
    <link>https://dev.to/alzhi_f93e67fa45b972</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2662567%2F21a8587a-f22f-4f4b-acf6-e0c17d9ee873.jpeg</url>
      <title>DEV Community: alexey.zh</title>
      <link>https://dev.to/alzhi_f93e67fa45b972</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/alzhi_f93e67fa45b972"/>
    <language>en</language>
    <item>
      <title>PostgreSQL to Go REST API, Generated</title>
      <dc:creator>alexey.zh</dc:creator>
      <pubDate>Sat, 09 May 2026 18:03:40 +0000</pubDate>
      <link>https://dev.to/alzhi_f93e67fa45b972/i-got-tired-of-writing-crud-by-hand-so-i-built-a-go-code-generator-from-a-database-1h52</link>
      <guid>https://dev.to/alzhi_f93e67fa45b972/i-got-tired-of-writing-crud-by-hand-so-i-built-a-go-code-generator-from-a-database-1h52</guid>
      <description>&lt;p&gt;Writing business logic can be interesting.&lt;/p&gt;

&lt;p&gt;Writing yet another REST wrapper around a table is not.&lt;/p&gt;

&lt;p&gt;Never.&lt;/p&gt;

&lt;p&gt;Ever.&lt;/p&gt;

&lt;p&gt;This is not software development; it is digital sock knitting. Except the socks then need to be covered with tests, documented, wrapped into DTOs, passed through a service, a repository, a handler, a mapper, a payload, and, preferably, completed without crying in the process.&lt;/p&gt;

&lt;p&gt;Imagine a very ordinary situation.&lt;/p&gt;

&lt;p&gt;A DBA adds a new table to the database. From the SQL side, everything is beautiful: a clean schema, proper indexes, relationships, constraints, the whole package. The DBA is happy. The database is happy. Somewhere far away, a tiny bird is singing.&lt;/p&gt;

&lt;p&gt;Meanwhile, the backend developer realizes two things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;They now need to create a controller, service, repository, interfaces, CRUD implementation, entity, DTO, payload, mappers, documentation, and probably something else, because “that is how we do things here.”&lt;/li&gt;
&lt;li&gt;They urgently need to reconsider their career path. Maybe become a painter. Or a baker. Or a person who herds goats and has no idea what &lt;code&gt;CreateUserRequest&lt;/code&gt; is.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;And that is only one table.&lt;/p&gt;

&lt;p&gt;When was the last time you saw a database with just one table?&lt;/p&gt;

&lt;p&gt;Usually, things are much more entertaining. Especially when the database was originally built as a large standalone project, and the backend and frontend were postponed until “later.” This is a beautiful architectural approach: first build the city, then think about roads, electricity, and maybe a fountain.&lt;/p&gt;

&lt;p&gt;You get the password to the DEV environment.&lt;/p&gt;

&lt;p&gt;You connect.&lt;/p&gt;

&lt;p&gt;And there you see several hundred tables, a couple dozen schemas, historical artifacts, a &lt;code&gt;users2&lt;/code&gt; table, a &lt;code&gt;users_new&lt;/code&gt; table, a &lt;code&gt;users_final&lt;/code&gt; table, a &lt;code&gt;users_final_really&lt;/code&gt; table, and also a reference dictionary that nobody has touched since 2016 because “it works.”&lt;/p&gt;

&lt;p&gt;And that is still considered a small database.&lt;/p&gt;

&lt;p&gt;At this point, you once again want to change jobs, move to the forest, raise livestock, grow tomatoes, and explain to your children that you used to write REST APIs, but now you have a normal life.&lt;/p&gt;

&lt;h2&gt;
  
  
  Routine Code Is Not Heroism
&lt;/h2&gt;

&lt;p&gt;I love writing code. I am a programmer.&lt;/p&gt;

&lt;p&gt;But I do not love monkey work disguised as “just a normal task for a couple of days.”&lt;/p&gt;

&lt;p&gt;Writing another handler is not hard. Writing another service is not hard. Writing another repository is not hard either.&lt;/p&gt;

&lt;p&gt;That is exactly the problem.&lt;/p&gt;

&lt;p&gt;You have already done it a thousand times. You know where &lt;code&gt;GetByID&lt;/code&gt; will be. You know where &lt;code&gt;Create&lt;/code&gt; will be. You know that somewhere nearby there will be &lt;code&gt;Update&lt;/code&gt;, then &lt;code&gt;Delete&lt;/code&gt;, then “let’s add filtering,” then “let’s add pagination,” then “why is this field not named the same way as on the frontend?”&lt;/p&gt;

&lt;p&gt;And you sit there thinking:&lt;br&gt;
am I really a software engineer, or just a very expensive template copier?&lt;/p&gt;

&lt;p&gt;Business logic is interesting.&lt;br&gt;
Architectural decisions are interesting.&lt;br&gt;
Understanding the domain is interesting.&lt;br&gt;
Writing the twenty-seventh CRUD layer for the &lt;code&gt;dict_operation_status&lt;/code&gt; table is not a spiritual journey. It is punishment.&lt;/p&gt;
&lt;h2&gt;
  
  
  So the Generator Appeared
&lt;/h2&gt;

&lt;p&gt;With these thoughts in mind, while starting another project, I decided to spend a couple of weeks not writing CRUD by hand for the first 100 tables, but building a code generator instead.&lt;/p&gt;

&lt;p&gt;At first, it was for Java.&lt;/p&gt;

&lt;p&gt;This is not about generating business logic. There is no magic here. The generator does not know how your business should work, why an order cannot be canceled after payment, or why production should not be touched after 6 PM on Friday, even though everyone still touches it anyway.&lt;/p&gt;

&lt;p&gt;This is only about primitive CRUD.&lt;/p&gt;

&lt;p&gt;That same layer that is almost always needed, but writing it by hand every time feels like manually moving bytes from one folder to another while commenting on the process in Jira.&lt;/p&gt;

&lt;p&gt;And, as practice showed, the idea was not wasted.&lt;/p&gt;

&lt;p&gt;Instead of heroically suffering for several weeks, you can generate the foundation, open the project in your IDE, and do normal work. Or at least a more meaningful form of suffering.&lt;/p&gt;
&lt;h2&gt;
  
  
  Then Go Came Along
&lt;/h2&gt;

&lt;p&gt;Later, I started working on Go projects, and I decided to adapt the same logic there.&lt;/p&gt;

&lt;p&gt;In some ways, Java is simpler. Project structure usually lives by the principle: “everything must be here, named exactly like this, built with Maven, and please do not ask unnecessary questions.”&lt;/p&gt;

&lt;p&gt;Go is closer to me. It is simpler, freer, and gives more room for creativity.&lt;/p&gt;

&lt;p&gt;Of course, along with creativity comes the other side of freedom: ten logging libraries, ten HTTP libraries, ten opinions about project structure, and twenty people in the comments explaining why your version is wrong.&lt;/p&gt;

&lt;p&gt;After reading forums, GitHub, other people’s projects, and surviving a mild existential crisis, the structure gradually started to take shape.&lt;/p&gt;

&lt;p&gt;Generation could begin.&lt;/p&gt;
&lt;h2&gt;
  
  
  “Just Generate an Entity” Sounds Easy
&lt;/h2&gt;

&lt;p&gt;Collecting the list of tables from a database and turning them into Go structs is not that hard.&lt;/p&gt;

&lt;p&gt;Well, almost. And those structs are not really useful on their own anyway; you need the full CRUD layer with methods and everything else.&lt;/p&gt;

&lt;p&gt;And then the real database begins.&lt;/p&gt;

&lt;p&gt;And in a real database, you have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;multiple schemas&lt;/li&gt;
&lt;li&gt;identical table names in different schemas&lt;/li&gt;
&lt;li&gt;tables without primary keys, because “it is fine”&lt;/li&gt;
&lt;li&gt;composite primary keys&lt;/li&gt;
&lt;li&gt;data types that stare at you like ancient gods&lt;/li&gt;
&lt;li&gt;legacy decisions that nobody understands but everyone is afraid to delete&lt;/li&gt;
&lt;li&gt;column names that make you want to call both a linguist and a therapist&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And that is where CRUD stops being so simple.&lt;/p&gt;

&lt;p&gt;But most problems are solvable. Over time, roughly 90% of typical cases can be covered. The remaining 10% can be finished by hand, and that is fine.&lt;/p&gt;

&lt;p&gt;The important part is that the main routine is already done.&lt;/p&gt;

&lt;p&gt;That means you no longer need to manually create hundreds of nearly identical files while pretending this is “backend layer development.”&lt;/p&gt;
&lt;h2&gt;
  
  
  Why This Is Actually Usable
&lt;/h2&gt;

&lt;p&gt;I ran the generator against several production databases that were large enough, complex enough, and honest enough.&lt;/p&gt;

&lt;p&gt;And I came to the conclusion: yes, this can be used.&lt;/p&gt;

&lt;p&gt;The generated code is easy to refactor in an IDE. Today this is not a big problem: rename packages, move files around, adjust the structure, make it fit the project style.&lt;/p&gt;

&lt;p&gt;The important thing is that the starting routine is already closed.&lt;/p&gt;

&lt;p&gt;That same layer that made you want to run away to the forest and grow tomatoes in the morning is already sitting in the project.&lt;/p&gt;

&lt;p&gt;Not perfect. Not the final architecture of your dreams. But good enough to start working instead of slowly turning into a boilerplate-code generator running on biological fuel.&lt;/p&gt;
&lt;h2&gt;
  
  
  A Small Example
&lt;/h2&gt;

&lt;p&gt;For each table, we generate a package under &lt;code&gt;internal/api/&amp;lt;schema&amp;gt;/&amp;lt;table&amp;gt;/&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;internal/api/
  repository.go          # top-level repository interfaces (all tables)
  service.go             # top-level service interfaces (all tables)
  handler.go             # router: mounts all routes

  public/
    products/
      entity.go          # DB struct mapped from table schema
      repository.go      # pgx queries: Save, Update, Delete, Find, FindAll, paginated
      service.go         # business logic layer, delegates to repository
      dto.go             # CreateDto, UpdateDto, Dto (internal transfer types)
      payload.go         # HTTP request/response types with JSON tags
      handler.go         # net/http handlers with Swagger annotations
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let’s look at generation for a table like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;create&lt;/span&gt; &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="n"&gt;products&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;record_id&lt;/span&gt;   &lt;span class="nb"&gt;serial&lt;/span&gt; &lt;span class="k"&gt;primary&lt;/span&gt; &lt;span class="k"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;category_id&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;          &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt; &lt;span class="k"&gt;references&lt;/span&gt; &lt;span class="n"&gt;categories&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;record_id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt;        &lt;span class="nb"&gt;varchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;250&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;description&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;comment&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="n"&gt;products&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="s1"&gt;'Stores products with a reference to their category.'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;comment&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="k"&gt;column&lt;/span&gt; &lt;span class="n"&gt;products&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="s1"&gt;'Name of the product.'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We get:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;entity.go&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;Products&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;RecordID&lt;/span&gt;    &lt;span class="kt"&gt;int&lt;/span&gt;      &lt;span class="s"&gt;`json:"record_id"    db:"record_id"`&lt;/span&gt;
    &lt;span class="n"&gt;CategoryID&lt;/span&gt;  &lt;span class="kt"&gt;int&lt;/span&gt;      &lt;span class="s"&gt;`json:"category_id"  db:"category_id"`&lt;/span&gt;
    &lt;span class="n"&gt;Name&lt;/span&gt;        &lt;span class="kt"&gt;string&lt;/span&gt;   &lt;span class="s"&gt;`json:"name"         db:"name"`&lt;/span&gt;
    &lt;span class="n"&gt;Description&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;  &lt;span class="s"&gt;`json:"description"  db:"description"`&lt;/span&gt;
    &lt;span class="n"&gt;CreatedAt&lt;/span&gt;   &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Time&lt;/span&gt; &lt;span class="s"&gt;`json:"created_at"  db:"created_at"`&lt;/span&gt;
    &lt;span class="n"&gt;UpdatedAt&lt;/span&gt;   &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Time&lt;/span&gt; &lt;span class="s"&gt;`json:"updated_at"  db:"updated_at"`&lt;/span&gt;
    &lt;span class="n"&gt;GUID&lt;/span&gt;        &lt;span class="kt"&gt;string&lt;/span&gt;   &lt;span class="s"&gt;`json:"guid"         db:"guid"`&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;repository.go&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;Save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;inputEntity&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;Products&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;Products&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="s"&gt;`
        insert into public.products (category_id, name, description)
        values ($1, $2, $3)
        returning record_id, category_id, name, description, created_at, updated_at, guid
    `&lt;/span&gt;
    &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Pool&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;QueryRow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;inputEntity&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CategoryID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;inputEntity&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;inputEntity&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;scanFullRow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;handler.go&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// @Summary Create new item&lt;/span&gt;
&lt;span class="c"&gt;// @Tags products&lt;/span&gt;
&lt;span class="c"&gt;// @Accept json&lt;/span&gt;
&lt;span class="c"&gt;// @Produce json&lt;/span&gt;
&lt;span class="c"&gt;// @Param request body productsCreateRequest true "Create input"&lt;/span&gt;
&lt;span class="c"&gt;// @Success 201 {object} productsResponse&lt;/span&gt;
&lt;span class="c"&gt;// @Router /api/v1/products [post]&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;Handler&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;Save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ResponseWriter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;req&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;productsCreateRequest&lt;/span&gt;&lt;span class="p"&gt;{}&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;httputils&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ReadJSON&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;httputils&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WriteJSON&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusBadRequest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;httputils&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ErrorResponse&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;Message&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;()})&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="c"&gt;// validate -&amp;gt; map to DTO -&amp;gt; call service -&amp;gt; map to response&lt;/span&gt;
    &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;svc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;mapCreateRequestToCreateInputDto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="o"&gt;...&lt;/span&gt;
    &lt;span class="n"&gt;httputils&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WriteJSON&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusCreated&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dtoToPayload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And a basic set of routes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;POST   /api/v1/products
PUT    /api/v1/products/{record_id}
DELETE /api/v1/products/{record_id}
GET    /api/v1/products/{record_id}
GET    /api/v1/products
GET    /api/v1/products/pageable
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everything is fairly simple, predictable, and clear: what goes where and why.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Put This in Open Source
&lt;/h2&gt;

&lt;p&gt;Honestly, because it seems like this might be useful not only to me.&lt;/p&gt;

&lt;p&gt;Many projects share the same pain: the database already exists, there are many tables, the API was needed yesterday, and for some reason the team wants to work on things that actually bring value instead of manually writing &lt;code&gt;repository.go&lt;/code&gt; number 148.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;gofromdb&lt;/code&gt; lets you quickly generate Go code from an existing database and get a foundation for further development.&lt;/p&gt;

&lt;p&gt;Not instead of the developer.&lt;/p&gt;

&lt;p&gt;Instead of the part of the developer that already runs on autopilot while sadly staring at the monitor.&lt;/p&gt;

&lt;p&gt;You run the generator and get the templates.&lt;br&gt;
Then you can write business logic, normalize the architecture, add rules, tests, validation, authorization, and everything that actually depends on the project.&lt;/p&gt;

&lt;p&gt;Instead of sitting there and manually proving to the computer that the &lt;code&gt;orders&lt;/code&gt; table really does need a &lt;code&gt;GetOrderByID&lt;/code&gt; method.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Comes Next
&lt;/h2&gt;

&lt;p&gt;I see several possible directions for the project:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;add a smarter type-handling system&lt;/li&gt;
&lt;li&gt;add generation of tests and mocks&lt;/li&gt;
&lt;li&gt;rethink the overall project structure and naming rules once again&lt;/li&gt;
&lt;li&gt;improve the templates&lt;/li&gt;
&lt;li&gt;remove what is unnecessary&lt;/li&gt;
&lt;li&gt;add what is necessary&lt;/li&gt;
&lt;li&gt;start growing tomatoes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I am especially interested in feedback on the project structure. In Go, this is always a lively topic, because every developer knows the one true project structure, and each of them has a different one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Words
&lt;/h2&gt;

&lt;p&gt;The project is here:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/hashmap-kz/gofromdb" rel="noopener noreferrer"&gt;https://github.com/hashmap-kz/gofromdb&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It runs with a single command. It should work correctly. At least, that is how every optimistic README usually begins.&lt;/p&gt;

&lt;p&gt;If it looks interesting, try it.&lt;/p&gt;

&lt;p&gt;If it does not work, open an issue, and I will try to help.&lt;/p&gt;

&lt;p&gt;If it does work, write as well. Sometimes an open-source author needs to know that their project was not only downloaded by a CI bot, but also launched by at least one living person with a pulse who did not run away to the forest.&lt;/p&gt;

&lt;p&gt;Good coding!&lt;/p&gt;

</description>
      <category>database</category>
      <category>go</category>
      <category>rest</category>
      <category>postgres</category>
    </item>
    <item>
      <title>Stop Shipping Breaking Go APIs by Accident</title>
      <dc:creator>alexey.zh</dc:creator>
      <pubDate>Wed, 06 May 2026 16:51:35 +0000</pubDate>
      <link>https://dev.to/alzhi_f93e67fa45b972/stop-shipping-breaking-go-apis-by-accident-4nln</link>
      <guid>https://dev.to/alzhi_f93e67fa45b972/stop-shipping-breaking-go-apis-by-accident-4nln</guid>
      <description>&lt;p&gt;Every Go release has one question that matters more than the diff itself:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Did we break something users compile against?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A pull request can look harmless. A few files changed, a type moved, a method now returns an error, a struct field disappeared. The commit history may look clean. The changelog may sound reasonable.&lt;/p&gt;

&lt;p&gt;But users do not depend on your commit messages.&lt;/p&gt;

&lt;p&gt;They depend on your &lt;strong&gt;public Go API&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That is the purpose of &lt;a href="https://github.com/hashmap-kz/relimpact" rel="noopener noreferrer"&gt;&lt;code&gt;relimpact&lt;/code&gt;&lt;/a&gt;: a small, fast tool that compares two Git refs and reports what changed in the exported Go API.&lt;/p&gt;

&lt;p&gt;Not every file.&lt;br&gt;
Not every commit.&lt;br&gt;
Not every line.&lt;/p&gt;

&lt;p&gt;Only the public API surface that users can import, call, implement, or compile against.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why another report?
&lt;/h2&gt;

&lt;p&gt;A raw &lt;code&gt;git diff&lt;/code&gt; is great when you want to inspect implementation details.&lt;/p&gt;

&lt;p&gt;A changelog is great when you want to explain a release to humans.&lt;/p&gt;

&lt;p&gt;But neither of them is the best tool for answering:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Which public Go symbols changed between these two refs?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That question needs a different view.&lt;/p&gt;

&lt;p&gt;For example, this is what matters before a release:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gd"&gt;- func Load(path string) *Config
&lt;/span&gt;&lt;span class="gi"&gt;+ func Load(path string) (*Config, error)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is not just “one line changed”.&lt;/p&gt;

&lt;p&gt;That is a breaking API change.&lt;/p&gt;

&lt;p&gt;And this is useful, but not breaking:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gi"&gt;+ func FromEnv(prefix string) (*Config, error)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is a new public API.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;relimpact&lt;/code&gt; separates those two ideas clearly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Breaking changes&lt;/strong&gt;: changed or removed public API.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;New API&lt;/strong&gt;: compatible additions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The result is a report that is easier to review in a pull request and easier to attach to a release.&lt;/p&gt;

&lt;h2&gt;
  
  
  It is not a diff tool
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;relimpact&lt;/code&gt; is intentionally narrow.&lt;/p&gt;

&lt;p&gt;It does not try to replace &lt;code&gt;git diff&lt;/code&gt;.&lt;br&gt;
It does not generate a raw changelog.&lt;br&gt;
It does not summarize commits.&lt;br&gt;
It does not care how many commits happened between two refs.&lt;/p&gt;

&lt;p&gt;Instead, it snapshots the exported Go API at one ref, snapshots it again at another ref, and compares the API surface.&lt;/p&gt;

&lt;p&gt;That means the report is based on &lt;strong&gt;API changes between refs&lt;/strong&gt;, not on commit messages.&lt;/p&gt;

&lt;p&gt;This distinction matters.&lt;/p&gt;

&lt;p&gt;A messy commit history can still produce a clean API report.&lt;/p&gt;

&lt;p&gt;A small commit can still produce a breaking public API change.&lt;/p&gt;

&lt;p&gt;That is the whole point.&lt;/p&gt;
&lt;h2&gt;
  
  
  What the report looks like
&lt;/h2&gt;

&lt;p&gt;A Markdown report is designed for pull request comments.&lt;/p&gt;

&lt;p&gt;It starts with a small compatibility summary, then puts breaking changes first:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbl3lit78u34s4qcd8owi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbl3lit78u34s4qcd8owi.png" alt=" " width="800" height="753"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;There is also an HTML report for CI artifacts and release review, but Markdown is usually the best format for pull requests.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fobdabyekuqotz7bdi7r5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fobdabyekuqotz7bdi7r5.png" alt=" " width="800" height="657"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyious20oqso0ovv2cey7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyious20oqso0ovv2cey7.png" alt=" " width="800" height="691"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Add it to a GitHub pull request
&lt;/h2&gt;

&lt;p&gt;Here is a simple GitHub Actions workflow that runs relimpact, generates a Markdown report, and posts it as a sticky pull request comment.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;API compatibility&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt; &lt;span class="nv"&gt;master&lt;/span&gt; &lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&lt;/span&gt;
  &lt;span class="na"&gt;pull-requests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;relimpact&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;

    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;fetch-depth&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-go@v5&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;go-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1.25"&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install relimpact&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;go install github.com/hashmap-kz/relimpact@latest&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Generate Markdown API report&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;relimpact \&lt;/span&gt;
            &lt;span class="s"&gt;--old="${{ github.event.pull_request.base.sha }}" \&lt;/span&gt;
            &lt;span class="s"&gt;--new="${{ github.event.pull_request.head.sha }}" \&lt;/span&gt;
            &lt;span class="s"&gt;--format=markdown \&lt;/span&gt;
            &lt;span class="s"&gt;--output api-report.md&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Comment API report on PR&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;marocchino/sticky-pull-request-comment@v2&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;header&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;relimpact-api-report&lt;/span&gt;
          &lt;span class="na"&gt;recreate&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
          &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;api-report.md&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is all.&lt;/p&gt;

&lt;p&gt;Every pull request gets a public API compatibility report.&lt;/p&gt;

&lt;p&gt;If nothing public has changed, the report stays quiet.&lt;/p&gt;

&lt;p&gt;If a method signature changed, a field disappeared, or a new exported type appeared, reviewers see it without digging through implementation diffs.&lt;/p&gt;

&lt;h2&gt;
  
  
  HTML as a CI artifact
&lt;/h2&gt;

&lt;p&gt;For release review, you may also want a browser-friendly report:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Generate HTML API report&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;relimpact \&lt;/span&gt;
            &lt;span class="s"&gt;--old="${{ github.event.pull_request.base.sha }}" \&lt;/span&gt;
            &lt;span class="s"&gt;--new="${{ github.event.pull_request.head.sha }}" \&lt;/span&gt;
            &lt;span class="s"&gt;--format=html \&lt;/span&gt;
            &lt;span class="s"&gt;--output api-report.html&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Upload HTML API report&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/upload-artifact@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;api-report&lt;/span&gt;
          &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;api-report.html&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The HTML report keeps the same structure as Markdown:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;verdict&lt;/li&gt;
&lt;li&gt;summary&lt;/li&gt;
&lt;li&gt;breaking changes&lt;/li&gt;
&lt;li&gt;new API&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The difference is presentation: package navigation, cleaner grouping, and a better browser view.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this matters
&lt;/h2&gt;

&lt;p&gt;Go makes public API changes feel deceptively simple.&lt;/p&gt;

&lt;p&gt;Changing a return value is easy.&lt;/p&gt;

&lt;p&gt;Removing a field is easy.&lt;/p&gt;

&lt;p&gt;Renaming a method is easy.&lt;/p&gt;

&lt;p&gt;But for users, those changes may mean failed builds, broken imports, or migration work.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;relimpact&lt;/code&gt; makes that visible before the release.&lt;/p&gt;

&lt;p&gt;It helps reviewers focus on the question that actually matters:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Are we changing the contract?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Easy to try
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;relimpact&lt;/code&gt; is a single binary.&lt;/p&gt;

&lt;p&gt;No server.&lt;br&gt;
No database.&lt;br&gt;
No external service.&lt;br&gt;
No AI.&lt;/p&gt;

&lt;p&gt;It works from your Git repository and compares two refs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;relimpact &lt;span class="nt"&gt;--old&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;v1.0.0 &lt;span class="nt"&gt;--new&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;HEAD
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Generate HTML:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;relimpact &lt;span class="nt"&gt;--old&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;v1.0.0 &lt;span class="nt"&gt;--new&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;HEAD &lt;span class="nt"&gt;--format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;html &lt;span class="nt"&gt;--output&lt;/span&gt; api-report.html
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Install with Go:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;go &lt;span class="nb"&gt;install &lt;/span&gt;github.com/hashmap-kz/relimpact@latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The project is here:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/hashmap-kz/relimpact" rel="noopener noreferrer"&gt;https://github.com/hashmap-kz/relimpact&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If the idea feels useful, starring the repository is always motivating. It helps show that small, focused Go tools still matter.&lt;/p&gt;

</description>
      <category>codereview</category>
      <category>go</category>
      <category>cleancode</category>
    </item>
    <item>
      <title>Finding Structurally Duplicate Go Functions with AST Hashing</title>
      <dc:creator>alexey.zh</dc:creator>
      <pubDate>Sun, 03 May 2026 15:50:48 +0000</pubDate>
      <link>https://dev.to/alzhi_f93e67fa45b972/finding-structurally-duplicate-go-functions-with-ast-hashing-4i26</link>
      <guid>https://dev.to/alzhi_f93e67fa45b972/finding-structurally-duplicate-go-functions-with-ast-hashing-4i26</guid>
      <description>&lt;p&gt;You know that feeling when you're reviewing a PR and you see a function that looks &lt;em&gt;suspiciously&lt;/em&gt; familiar? Same structure, different variable names, slightly different literals. Someone copy-pasted and tweaked it, and now there are two places to update every time something changes.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/hashmap-kz/godedup" rel="noopener noreferrer"&gt;godedup&lt;/a&gt; catches this automatically. It finds structurally duplicate functions in Go codebases - even when the copies have been superficially modified. This post is about the algorithms that make it work, and a few implementation details I found interesting to think through.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Problem With Text-Based Approaches
&lt;/h2&gt;

&lt;p&gt;The naive approach is text diffing. Take two functions, run them through a standard diff algorithm, see if they're similar enough. This immediately falls apart:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// Function A&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;UserRepo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;findByID&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="kt"&gt;int64&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;QueryRowContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"SELECT * FROM users WHERE id = $1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt; &lt;span class="n"&gt;User&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Scan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Email&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"findByID: %w"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;// Function B&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;OrderRepo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;findByID&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="kt"&gt;int64&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;QueryRowContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"SELECT * FROM orders WHERE id = $1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="n"&gt;Order&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Scan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Amount&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"findByID: %w"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Text diff says these are 60% different. A human says these are the same function. The structure - a query, a scan, an error return, a result return - is identical. Only the types and column names differ.&lt;/p&gt;

&lt;p&gt;What you actually want is to compare &lt;em&gt;shape&lt;/em&gt;, not text. That means going through the AST.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Core Insight: Normalize, Then Hash
&lt;/h2&gt;

&lt;p&gt;The key idea is that two functions are structurally equivalent if their AST subtrees are &lt;em&gt;isomorphic&lt;/em&gt; - same shape, same node types, same operators, same control flow - after you normalize away the parts that don't matter.&lt;/p&gt;

&lt;p&gt;What doesn't matter for structural comparison:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Variable and parameter names (&lt;code&gt;userID&lt;/code&gt; vs &lt;code&gt;orderID&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;String literals (&lt;code&gt;"users"&lt;/code&gt; vs &lt;code&gt;"orders"&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Numeric literals (&lt;code&gt;1&lt;/code&gt; vs &lt;code&gt;42&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Package qualifiers (&lt;code&gt;fmt.Println&lt;/code&gt; vs &lt;code&gt;log.Println&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What &lt;em&gt;does&lt;/em&gt; matter:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Node types (&lt;code&gt;IfStmt&lt;/code&gt;, &lt;code&gt;ForStmt&lt;/code&gt;, &lt;code&gt;ReturnStmt&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Operators (&lt;code&gt;+&lt;/code&gt; vs &lt;code&gt;-&lt;/code&gt; are different)&lt;/li&gt;
&lt;li&gt;Control flow structure (a loop containing an if is different from an if containing a loop)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;nil&lt;/code&gt;, &lt;code&gt;true&lt;/code&gt;, &lt;code&gt;false&lt;/code&gt; - these have semantic meaning&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The implementation is a recursive hash function over the AST. For each node type, it combines a stable ID for the node type with the hashes of its children:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;Hasher&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;hashNode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt; &lt;span class="n"&gt;ast&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Node&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;uint64&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;switch&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;type&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;ast&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IfStmt&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;combine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nodeID&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"IfStmt"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hashNode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Init&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hashNode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Cond&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hashNode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Body&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hashNode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Else&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;ast&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Ident&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
        &lt;span class="c"&gt;// Normalize: all identifiers hash identically&lt;/span&gt;
        &lt;span class="c"&gt;// EXCEPT nil/true/false which have semantic meaning&lt;/span&gt;
        &lt;span class="k"&gt;switch&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="s"&gt;"nil"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;   &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;nodeID&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"nil"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="s"&gt;"true"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;nodeID&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"true"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="s"&gt;"false"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;nodeID&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"false"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;nodeID&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Ident"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c"&gt;// all other identifiers are equivalent&lt;/span&gt;

    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;ast&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BasicLit&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
        &lt;span class="c"&gt;// Normalize: literals hash only by kind, not value&lt;/span&gt;
        &lt;span class="c"&gt;// "users" and "orders" produce the same hash&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;combine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nodeID&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"BasicLit"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="kt"&gt;uint64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Kind&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;ast&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SelectorExpr&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
        &lt;span class="c"&gt;// Normalize package qualifier: only the selected name matters&lt;/span&gt;
        &lt;span class="c"&gt;// fmt.Println and log.Println hash identically&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;combine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nodeID&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"SelectorExpr"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;nodeID&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sel&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="c"&gt;// ...&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;nodeID&lt;/code&gt; function maps a string name to a stable &lt;code&gt;uint64&lt;/code&gt; using the first 8 bytes of its SHA-256 hash. The &lt;code&gt;combine&lt;/code&gt; function mixes multiple values using FNV-style multiplication - fast, good avalanche, and order-dependent (so &lt;code&gt;[A, B]&lt;/code&gt; and &lt;code&gt;[B, A]&lt;/code&gt; produce different hashes):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;combine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;vals&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="kt"&gt;uint64&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;uint64&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="kt"&gt;uint64&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0xcbf29ce484222325&lt;/span&gt; &lt;span class="c"&gt;// FNV offset basis&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;vals&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="o"&gt;^=&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;
        &lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="o"&gt;*=&lt;/span&gt; &lt;span class="m"&gt;0x100000001b3&lt;/span&gt; &lt;span class="c"&gt;// FNV prime&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After hashing, both &lt;code&gt;findByID&lt;/code&gt; functions above produce the same &lt;code&gt;uint64&lt;/code&gt;. Detecting exact clones is then just grouping by hash - O(n) with a map.&lt;/p&gt;




&lt;h2&gt;
  
  
  Two Representations Per Function
&lt;/h2&gt;

&lt;p&gt;Here's a design decision that enables both exact &lt;em&gt;and&lt;/em&gt; near clone detection from a single parse pass.&lt;/p&gt;

&lt;p&gt;Each function gets two hash representations stored in &lt;code&gt;FuncInfo&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;FuncInfo&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;TopHash&lt;/span&gt;  &lt;span class="kt"&gt;uint64&lt;/span&gt;   &lt;span class="c"&gt;// hash of the entire function body&lt;/span&gt;
    &lt;span class="n"&gt;StmtSeq&lt;/span&gt;  &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;uint64&lt;/span&gt; &lt;span class="c"&gt;// per-statement hashes&lt;/span&gt;
    &lt;span class="c"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;TopHash&lt;/code&gt; is the hash of the complete function - used for exact clone detection. If two functions have the same &lt;code&gt;TopHash&lt;/code&gt;, they're structurally identical.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;StmtSeq&lt;/code&gt; is a slice where each element is the hash of one top-level statement. This is what enables near-clone detection.&lt;/p&gt;

&lt;p&gt;Computing both is trivial:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;stmtSeq&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;make&lt;/span&gt;&lt;span class="p"&gt;([]&lt;/span&gt;&lt;span class="kt"&gt;uint64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fn&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Body&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stmt&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;fn&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Body&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;List&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;stmtSeq&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stmtSeq&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hashNode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stmt&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;topHash&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;hashUint64Slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stmtSeq&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;TopHash&lt;/code&gt; is derived from &lt;code&gt;StmtSeq&lt;/code&gt; - it's the hash of the sequence of statement hashes. So you get both representations for the cost of one AST traversal.&lt;/p&gt;




&lt;h2&gt;
  
  
  Near-Clone Detection: Edit Distance on Hash Sequences
&lt;/h2&gt;

&lt;p&gt;Two functions are near-clones if one has a few extra or different statements compared to the other. The canonical algorithm for "how many insertions/deletions does it take to transform sequence A into sequence B" is Levenshtein edit distance.&lt;/p&gt;

&lt;p&gt;The twist: instead of computing edit distance on characters or lines, we compute it on the &lt;code&gt;StmtSeq&lt;/code&gt; - the sequence of statement hashes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;editDistance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;uint64&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;la&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lb&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;dp&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;make&lt;/span&gt;&lt;span class="p"&gt;([][]&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;la&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;dp&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;dp&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;make&lt;/span&gt;&lt;span class="p"&gt;([]&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lb&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;la&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;dp&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;lb&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;dp&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;la&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;lb&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;dp&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dp&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="c"&gt;// statements are structurally identical&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;dp&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dp&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;dp&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;dp&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;dp&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;la&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="n"&gt;lb&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Similarity is then normalized to &lt;code&gt;[0.0, 1.0]&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;Similarity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;FuncInfo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;float64&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;dist&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;editDistance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StmtSeq&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StmtSeq&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;maxLen&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StmtSeq&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StmtSeq&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="m"&gt;1.0&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="kt"&gt;float64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dist&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="kt"&gt;float64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;maxLen&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This means: if two functions share 8 out of 10 statements (by structure), they score 0.80. The default threshold is 0.85, so that pair would not be reported - you need at least 85% structural overlap.&lt;/p&gt;

&lt;p&gt;The practical effect: adding a logging statement or an extra validation check doesn't make two otherwise-identical functions invisible to the detector. The near-clone detection catches exactly the "same function, one copy got an extra guard clause" pattern.&lt;/p&gt;




&lt;h2&gt;
  
  
  Grouping Near-Clones: Union-Find
&lt;/h2&gt;

&lt;p&gt;If function A is 90% similar to B, and B is 88% similar to C, then A, B, and C probably all belong to the same clone group. Union-Find handles this transitivity correctly.&lt;/p&gt;

&lt;p&gt;The pairwise comparison is O(n^2) - for every pair of candidate functions (those not already in an exact clone group), compute similarity and union them if above threshold:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;candidates&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;candidates&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;candidates&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;candidates&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

        &lt;span class="c"&gt;// fast pre-filter: skip pairs with very different statement counts&lt;/span&gt;
        &lt;span class="n"&gt;ratio&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="kt"&gt;float64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NumStmts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="kt"&gt;float64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NumStmts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;ratio&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="m"&gt;0.7&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;ratio&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;1.43&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="n"&gt;sim&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;hash&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Similarity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;sim&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MinSimilarity&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;union&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sim&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The statement count pre-filter is worth noting: if function A has 5 statements and function B has 20, they can't possibly score above 0.75 similarity (at best 5 matching statements out of 20). The filter skips pairs where the ratio is outside [0.7, 1.43] before doing the O(n·m) dynamic programming - significant in practice.&lt;/p&gt;

&lt;p&gt;A subtle bug to be aware of: when you later compute the minimum similarity across a group, the similarity map is keyed by &lt;code&gt;[2]string{a.Name, b.Name}&lt;/code&gt; in insertion order. But when you iterate over the group (which comes from a map), order is random. So you need to try both orderings:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;na&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nb&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;group&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;group&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;similarity&lt;/span&gt;&lt;span class="p"&gt;[[&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;na&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nb&lt;/span&gt;&lt;span class="p"&gt;}];&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;minSim&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;minSim&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;similarity&lt;/span&gt;&lt;span class="p"&gt;[[&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;nb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;na&lt;/span&gt;&lt;span class="p"&gt;}];&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;minSim&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;minSim&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Miss this and near-clone groups all report 100% similarity regardless of actual score.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Two-Pass Architecture
&lt;/h2&gt;

&lt;p&gt;Detection runs in two sequential passes:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pass 1 - Exact clones, O(n):&lt;/strong&gt;&lt;br&gt;
Group all functions by &lt;code&gt;TopHash&lt;/code&gt;. Any group with 2+ members is an exact clone group. Mark all members so they're excluded from Pass 2.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pass 2 - Near clones, O(n^2):&lt;/strong&gt;&lt;br&gt;
Only compare functions &lt;em&gt;not&lt;/em&gt; already in an exact clone group. This is both a correctness choice (exact clones would trivially satisfy the near-clone threshold and pollute groups) and a performance choice (exact clones are often numerous - &lt;code&gt;validate()&lt;/code&gt; copied across 10 packages - and skipping them keeps the O(n^2) set small).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// Pass 1: O(n) exact detection&lt;/span&gt;
&lt;span class="n"&gt;exactGroups&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;make&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;uint64&lt;/span&gt;&lt;span class="p"&gt;][]&lt;/span&gt;&lt;span class="n"&gt;hash&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FuncInfo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;funcs&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;exactGroups&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TopHash&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exactGroups&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TopHash&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;// Pass 2: O(n^2) near detection on remaining functions&lt;/span&gt;
&lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;candidates&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="n"&gt;hash&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FuncInfo&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;funcs&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;inExactClone&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;candidates&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;candidates&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  What It Finds in Practice
&lt;/h2&gt;

&lt;p&gt;Running it on a real codebase immediately found things worth fixing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GROUP  TYPE   SIM   FUNCTION                          LOCATION                              STMTS  LINES
-----------------------------------------------------------------------------------------------------
1      EXACT  100%  auth.(*UserStore).GetUserByID     internal/auth/user_store.go:102       7      24
1      EXACT  100%  auth.(*UserStore).GetUserByEmail  internal/auth/user_store.go:127       7      23
1      EXACT  100%  auth.(*UserStore).GetUserByName   internal/auth/user_store.go:151       7      22
------------------------------------------------------------------------------------------------------
2      EXACT  100%  http.disableClientCache           internal/http/router.go:45            3      5
2      EXACT  100%  branding.disableClientCache       internal/wiki/branding/routes.go:249  3      5
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first group is three database query functions - same structure, different WHERE clauses. They should be one generic function or at least share a helper. The second is a middleware function that got copy-pasted into two packages instead of being placed in a shared location.&lt;/p&gt;

&lt;p&gt;Both are actionable. Neither would have been caught by a linter.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;A few directions worth exploring:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Baseline support&lt;/strong&gt; - the biggest adoption blocker for existing codebases is that there are already dozens of clones accumulated over years. A &lt;code&gt;--save-baseline&lt;/code&gt; / &lt;code&gt;--diff-baseline&lt;/code&gt; flag would let teams adopt the tool without failing CI on pre-existing debt.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SARIF output&lt;/strong&gt; - SARIF is the standard format for GitHub Code Scanning. One output flag and findings appear as inline PR annotations with file links. Roughly 50 lines of output code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;LSH for scale&lt;/strong&gt; - the O(n^2) near-clone pass starts showing latency on codebases with 5000+ functions. Locality-Sensitive Hashing on the &lt;code&gt;StmtSeq&lt;/code&gt; arrays would reduce it to near-O(n) by only comparing functions that land in the same hash bucket.&lt;/p&gt;




&lt;p&gt;The tool is at &lt;a href="https://github.com/hashmap-kz/godedup" rel="noopener noreferrer"&gt;github.com/hashmap-kz/godedup&lt;/a&gt; - &lt;code&gt;go install github.com/hashmap-kz/godedup@latest&lt;/code&gt; and point it at any Go project. It runs in seconds and exits 0, so there's no friction in trying it.&lt;/p&gt;

&lt;p&gt;If you hit false positives or miss cases you expected to catch, issues are open. The normalization rules are the most interesting part to tune.&lt;/p&gt;

</description>
      <category>go</category>
      <category>algorithms</category>
      <category>devtools</category>
      <category>programming</category>
    </item>
    <item>
      <title>Don’t Trust Backups You Haven’t Restored</title>
      <dc:creator>alexey.zh</dc:creator>
      <pubDate>Sun, 26 Apr 2026 14:39:58 +0000</pubDate>
      <link>https://dev.to/alzhi_f93e67fa45b972/backups-are-useless-until-you-restore-them-50do</link>
      <guid>https://dev.to/alzhi_f93e67fa45b972/backups-are-useless-until-you-restore-them-50do</guid>
      <description>&lt;h2&gt;
  
  
  0. A Backup Is Not a File, but a Promise
&lt;/h2&gt;

&lt;p&gt;You can write anything you want in the logs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;backup completed successfully
WAL uploaded successfully
retention completed successfully
archive is healthy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But on the day of a real disaster, PostgreSQL will not read your beautiful logs.&lt;br&gt;
It will simply ask for the next WAL file.&lt;/p&gt;

&lt;p&gt;And if you cannot provide it, the whole story ends right there.&lt;/p&gt;

&lt;p&gt;This article is not about the internal implementation of a &lt;a href="https://github.com/pgrwl/pgrwl" rel="noopener noreferrer"&gt;WAL receiver&lt;/a&gt;.&lt;br&gt;
There was already a &lt;a href="https://dev.to/alzhi_f93e67fa45b972/a-long-story-about-how-i-dug-into-the-postgresql-source-code-to-write-my-own-wal-receiver-and-what-1648"&gt;separate long story about that&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This article is about how I see the future development of the tool.&lt;/p&gt;

&lt;p&gt;The real question is this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Can I restore PostgreSQL from what my tool has been so confidently saving?&lt;/p&gt;
&lt;/blockquote&gt;


&lt;h2&gt;
  
  
  1. Backups Are a Comforting Lie
&lt;/h2&gt;

&lt;p&gt;Backups are one of the most comforting illusions in infrastructure.&lt;/p&gt;

&lt;p&gt;The command completed successfully.&lt;br&gt;
A file appeared in S3.&lt;br&gt;
There is a green line in the logs.&lt;br&gt;
Maybe somewhere on a dashboard, &lt;code&gt;healthy&lt;/code&gt; is even glowing.&lt;/p&gt;

&lt;p&gt;Everyone feels relaxed and confident.&lt;br&gt;
The problem is that none of this proves that recovery is possible.&lt;/p&gt;

&lt;p&gt;It only proves that some operation completed without an error.&lt;br&gt;
Some bytes moved from one place to another.&lt;br&gt;
Some process returned exit code 0.&lt;br&gt;
Some &lt;code&gt;object storage&lt;/code&gt; API said, “yes, I accepted the file.”&lt;/p&gt;

&lt;p&gt;But the recovery process does not care about our bright feelings.&lt;/p&gt;

&lt;p&gt;It cares about only one thing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Give me the required WAL file.
Right now.
Under the correct name.
In the correct place.
And make sure it is not corrupted.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the file exists, PostgreSQL continues restoring history.&lt;/p&gt;

&lt;p&gt;If the file does not exist, the history ends.&lt;/p&gt;

&lt;p&gt;At that exact moment, “backup completed successfully” turns from a pleasant phrase into a question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;What exactly did you successfully do, actually?&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  2. WAL Files Are Not the Goal
&lt;/h2&gt;

&lt;p&gt;When you write a WAL receiver, it is easy to become emotionally attached to WAL files.&lt;/p&gt;

&lt;p&gt;They stream beautifully.&lt;br&gt;
They appear in a directory.&lt;br&gt;
They have serious-looking names like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;00000001000000000000000A
00000001000000000000000B
00000001000000000000000C
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;They are uploaded to remote storage.&lt;br&gt;
They can be compressed.&lt;br&gt;
They can be encrypted.&lt;br&gt;
They can be shown in a UI.&lt;br&gt;
They can be counted, sorted, checked, deleted, and downloaded again.&lt;/p&gt;

&lt;p&gt;At some point, it starts to feel like the project is about them.&lt;/p&gt;

&lt;p&gt;But that is a trap.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;WAL files are not the product.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The product is the restored database.&lt;/p&gt;

&lt;p&gt;Nobody wakes up at 3 a.m. thinking:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“How wonderful that I have 438 beautiful WAL files sitting in S3.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;People think differently:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Can I bring the database back before people start calling me?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A WAL archive is not a museum of artifacts.&lt;br&gt;
It does not exist to store pretty files with long names.&lt;/p&gt;

&lt;p&gt;It exists so PostgreSQL can replay the history of changes, the transaction log.&lt;/p&gt;

&lt;p&gt;In other words, it exists to restore the database state through the chain:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;base-backup -&amp;gt; WAL -&amp;gt; WAL -&amp;gt; WAL -&amp;gt; target recovery point
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without a base backup, WAL files are useless.&lt;/p&gt;

&lt;p&gt;Without WAL files, a base backup quickly becomes an outdated snapshot of the past.&lt;/p&gt;

&lt;p&gt;Without &lt;code&gt;restore_command&lt;/code&gt;, everything together turns into a collection of files that looks like a backup system, but has not yet proven that it can work as a backup system.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. The Real Product Is Point-in-Time Recovery
&lt;/h2&gt;

&lt;p&gt;PostgreSQL recovery is not “restoring one file.”&lt;/p&gt;

&lt;p&gt;It is restoring history.&lt;/p&gt;

&lt;p&gt;A base backup gives the database state at a specific moment.&lt;br&gt;
WAL files after that moment provide the history of changes.&lt;br&gt;
The recovery process applies that history up to the required point.&lt;/p&gt;

&lt;p&gt;In simplified form:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[base-backup]
      |
      v
000000010000000000000001
      |
      v
000000010000000000000002
      |
      v
000000010000000000000003
      |
      v
[target recovery point]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The problem is that the smallest gap in this chain can break everything.&lt;/p&gt;

&lt;p&gt;One missing WAL file, and PostgreSQL cannot continue replay.&lt;/p&gt;

&lt;p&gt;It is like losing one page from a legal contract.&lt;br&gt;
Except the contract is a production database, and the lawyer is PostgreSQL, which simply refuses to start any further.&lt;/p&gt;

&lt;p&gt;So the question is not:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Do I have WAL files?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The real question is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Do I have a continuous, recoverable WAL chain
starting from a known base backup?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is where &lt;code&gt;pgrwl&lt;/code&gt; stops being just a “WAL file receiver.”&lt;/p&gt;

&lt;p&gt;It becomes part of the recovery chain.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. &lt;code&gt;restore_command&lt;/code&gt; Is the Final Boss
&lt;/h2&gt;

&lt;p&gt;There is a moment in PostgreSQL recovery where all theory ends.&lt;/p&gt;

&lt;p&gt;That moment is &lt;code&gt;restore_command&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;On paper, everything looks beautiful:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;base-backup + WAL archive = point-in-time recovery
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But during recovery, PostgreSQL does not say:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Show me a beautiful dashboard.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It does not say:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Tell me how well the &lt;code&gt;uploader&lt;/code&gt; worked last week.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It does not ask:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Did you have logs and metrics?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It asks for a specific file.&lt;/p&gt;

&lt;p&gt;Something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;I need WAL file 00000001000000000000000A.
Put it here.
Return success if it worked.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And that is all.&lt;/p&gt;

&lt;p&gt;If &lt;code&gt;restore_command&lt;/code&gt; can fetch this file, recovery continues.&lt;/p&gt;

&lt;p&gt;If it cannot, the whole system stops being a recovery system and becomes a sad collection of partially useful data.&lt;/p&gt;

&lt;p&gt;That is why &lt;code&gt;restore_command&lt;/code&gt; is the final boss of a backup system.&lt;/p&gt;

&lt;p&gt;This is where it becomes clear whether the archive is actually usable for recovery.&lt;/p&gt;

&lt;p&gt;For the development of &lt;code&gt;pgrwl&lt;/code&gt;, this is an important shift in thinking.&lt;/p&gt;

&lt;p&gt;At first, it seems that the main thing is to receive WAL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PostgreSQL -&amp;gt; replication protocol -&amp;gt; pgrwl -&amp;gt; local directory
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then it seems that the main thing is to upload WAL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;local directory -&amp;gt; compression/encryption -&amp;gt; S3/SFTP
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And then comes the realization:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;S3/SFTP -&amp;gt; restore_command -&amp;gt; PostgreSQL recovery
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the moment of truth.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. A Successful Upload Is Not Proof of Successful Future Recovery
&lt;/h2&gt;

&lt;p&gt;Object storage makes people optimistic.&lt;/p&gt;

&lt;p&gt;You uploaded a file.&lt;br&gt;
The API returned a successful response.&lt;br&gt;
The file appeared in the bucket.&lt;br&gt;
Everything looks reliable.&lt;/p&gt;

&lt;p&gt;But for a backup system, that is not enough.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;upload successful&lt;/em&gt; proves only transport.&lt;/p&gt;

&lt;p&gt;It does not prove:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;that the file is complete&lt;/li&gt;
&lt;li&gt;that the checksum matches&lt;/li&gt;
&lt;li&gt;that encryption/decryption works&lt;/li&gt;
&lt;li&gt;that compression/decompression works&lt;/li&gt;
&lt;li&gt;that the file can be downloaded back&lt;/li&gt;
&lt;li&gt;that &lt;code&gt;restore_command&lt;/code&gt; will find it under the correct name&lt;/li&gt;
&lt;li&gt;that retention will not delete it tomorrow&lt;/li&gt;
&lt;li&gt;that the entire WAL chain is continuous&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is exactly why a backup tool must be unpleasantly suspicious.&lt;/p&gt;

&lt;p&gt;It is not enough to ask:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Can I upload it?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You need to ask:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Can I upload it?
Can I list it?
Can I read it back?
Can I decrypt it?
Can PostgreSQL use it during recovery?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;A backup uploaded to S3 but never downloaded back even once is a motivational poster, not a recovery strategy.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  6. Storage Is Where Backup Tools Become Dangerous
&lt;/h2&gt;

&lt;p&gt;Cleaning up files sounds tempting.&lt;/p&gt;

&lt;p&gt;Delete old files.&lt;br&gt;
Free up space.&lt;br&gt;
Clean the archive.&lt;/p&gt;

&lt;p&gt;What could possibly go wrong?&lt;/p&gt;

&lt;p&gt;In backup systems, almost everything.&lt;/p&gt;

&lt;p&gt;Deleting the wrong “old file” can turn the entire chain into a useless set of data.&lt;/p&gt;

&lt;p&gt;Bad cleanup logic thinks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Delete WAL files older than 7 days.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;More correct cleanup logic should think like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Delete only those WAL files that are definitely not needed
by any backup and by any recovery scenario.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The difference is huge.&lt;/p&gt;

&lt;p&gt;A WAL file may look old by timestamp, but still be required to recover from a specific base backup.&lt;/p&gt;

&lt;p&gt;If you delete a WAL that the oldest backup needs, that backup turns into beautiful garbage.&lt;/p&gt;

&lt;p&gt;Cleanup is not housekeeping.&lt;br&gt;
It is part of the recovery contract.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A backup tool should delete files with the confidence of a nervous accountant, not a shell script with &lt;code&gt;rm -rf&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;


&lt;h2&gt;
  
  
  7. Backup Tool Development Is Based on Negative Scenarios
&lt;/h2&gt;

&lt;p&gt;The most useful way to think about a backup system is not to start with positive scenarios.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;not with the &lt;code&gt;receive&lt;/code&gt; command&lt;/li&gt;
&lt;li&gt;not with a beautiful CLI&lt;/li&gt;
&lt;li&gt;not with a dashboard&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;But with a disaster.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Imagine this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;the primary is unavailable
the local disk failed
the directory disappeared
a new PostgreSQL must be brought up
it must be recovered to the required point
people are waiting
coffee no longer helps
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And now we ask the questions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Where is the base backup?
Which WAL files are needed?
Are they in storage?
Can they be downloaded?
Can they be decrypted?
Can they be decompressed?
Does PostgreSQL know how to get them through restore_command?
Are there gaps in the chain?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When you design a system from this point, many features stop being “nice to have.”&lt;/p&gt;

&lt;p&gt;A status API is not decoration.&lt;br&gt;
It is a way to understand whether the receiver is alive.&lt;/p&gt;

&lt;p&gt;WAL listing is not a toy for the UI.&lt;br&gt;
It is a way to check the archive.&lt;/p&gt;

&lt;p&gt;Backup metadata is not bureaucracy.&lt;br&gt;
It is a recovery map.&lt;/p&gt;

&lt;p&gt;Cleanup is not space saving.&lt;br&gt;
It is a potentially dangerous operation.&lt;/p&gt;

&lt;p&gt;Logging is not noise.&lt;br&gt;
It is evidence.&lt;/p&gt;

&lt;p&gt;A dashboard is not “for making things pretty.”&lt;br&gt;
It is a way to quickly answer the question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Are we okay, or do we just not yet know that we are already not okay?&lt;/p&gt;
&lt;/blockquote&gt;


&lt;h2&gt;
  
  
  8. What the Tool Should Know
&lt;/h2&gt;

&lt;p&gt;At the beginning, it seems enough for the tool to know only a little:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;where to read WAL from
where to write WAL
where to upload WAL
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then reality arrives with a long list of requirements.&lt;/p&gt;




&lt;h2&gt;
  
  
  9. What the Operator Should See
&lt;/h2&gt;

&lt;p&gt;A backup system should not require archaeology.&lt;/p&gt;

&lt;p&gt;If an operator needs to read 4,000 lines of logs to understand whether the archive is alive, the UX has already lost.&lt;/p&gt;

&lt;p&gt;An infrastructure UI does not have to look like a spaceship.&lt;/p&gt;

&lt;p&gt;It should quickly answer a few questions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;is the receiver alive?
is the WAL stream running?
what is the last received WAL?
what is the last uploaded WAL?
when was the last upload?
are there errors?
how many WAL files are stored locally?
how many WAL files are stored remotely?
which base backups exist?
which backup was the last successful one?
what will be required for recovery?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A green &lt;code&gt;healthy&lt;/code&gt; label does not mean much by itself.&lt;/p&gt;

&lt;p&gt;It is better to show evidence:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;last received WAL: 00000001000000000000000A
last uploaded WAL: 000000010000000000000009
last upload: 42 seconds ago
slot: pgrwl_slot
mode: receive
storage: s3
errors: none
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is why the UI for pgrwl is not just about “making it pretty.”&lt;/p&gt;

&lt;p&gt;It is an attempt to give the operator the state of the system without forcing them to read the tea leaves in logs.&lt;/p&gt;




&lt;h2&gt;
  
  
  10. Test Recovery Skeptically
&lt;/h2&gt;

&lt;p&gt;The only honest test of a backup system is recovery.&lt;/p&gt;

&lt;p&gt;Everything else is optimism.&lt;/p&gt;

&lt;p&gt;A good test should be rough.&lt;/p&gt;

&lt;p&gt;It should do unpleasant things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;create PostgreSQL&lt;/li&gt;
&lt;li&gt;generate data&lt;/li&gt;
&lt;li&gt;take a base backup&lt;/li&gt;
&lt;li&gt;continue writing data&lt;/li&gt;
&lt;li&gt;stream WAL&lt;/li&gt;
&lt;li&gt;upload WAL to storage&lt;/li&gt;
&lt;li&gt;stop everything&lt;/li&gt;
&lt;li&gt;delete the &lt;code&gt;data directory&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;restore the base backup into a new location&lt;/li&gt;
&lt;li&gt;configure &lt;code&gt;restore_command&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;replay WAL&lt;/li&gt;
&lt;li&gt;verify that the data is in place&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Even better if the tests can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;restart the receiver&lt;/li&gt;
&lt;li&gt;interrupt uploads&lt;/li&gt;
&lt;li&gt;switch modes&lt;/li&gt;
&lt;li&gt;generate WAL under load&lt;/li&gt;
&lt;li&gt;check for missing gaps&lt;/li&gt;
&lt;li&gt;compare expected and restored data&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A bad test:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;command exited with 0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A good test:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;new PostgreSQL instance started from restored backup
expected rows are present
target recovery point reached
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;If a backup test does not cause mild discomfort, it is probably a demo.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  11. Things I Still Do Not Trust
&lt;/h2&gt;

&lt;p&gt;There are parts of a backup system that I do not trust “just because.”&lt;/p&gt;

&lt;p&gt;Not because they are necessarily broken.&lt;/p&gt;

&lt;p&gt;But because they are too important to trust by default.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I do not trust &lt;code&gt;retention&lt;/code&gt; until I understand why a file can be deleted.&lt;/li&gt;
&lt;li&gt;I do not trust &lt;code&gt;encryption&lt;/code&gt; until I have verified &lt;code&gt;decryption&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;I do not trust &lt;code&gt;compression&lt;/code&gt; until I have verified &lt;code&gt;decompression&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;I do not trust &lt;code&gt;object storage&lt;/code&gt; until I have read the data back.&lt;/li&gt;
&lt;li&gt;I do not trust the &lt;code&gt;restore procedure&lt;/code&gt; until I have started PostgreSQL from a restored backup.&lt;/li&gt;
&lt;li&gt;I do not trust green badges on a dashboard if there are no concrete numbers behind them.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  12. Make Recovery Boring
&lt;/h2&gt;

&lt;p&gt;The goal of a backup system is not to make backups exciting.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The goal is to make recovery boring.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That means it should be:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;predictable&lt;/li&gt;
&lt;li&gt;documented&lt;/li&gt;
&lt;li&gt;verified&lt;/li&gt;
&lt;li&gt;observable&lt;/li&gt;
&lt;li&gt;repeatable&lt;/li&gt;
&lt;li&gt;without magic&lt;/li&gt;
&lt;li&gt;without heroism&lt;/li&gt;
&lt;li&gt;without “I think we also need to run this script here”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In an ideal world, recovery does not require a heroic engineer, three terminals, spiritual negotiations with &lt;code&gt;object storage&lt;/code&gt;, and a random shell script from 2018.&lt;/p&gt;

&lt;p&gt;It should be a procedure.&lt;/p&gt;

&lt;p&gt;A boring procedure.&lt;/p&gt;

&lt;p&gt;Because in infrastructure, boring is a compliment.&lt;/p&gt;

&lt;p&gt;Thank you for reading!&lt;/p&gt;

</description>
      <category>go</category>
      <category>postgres</category>
      <category>database</category>
    </item>
    <item>
      <title>WEB UI in Go? Nothing Can Stop Me!</title>
      <dc:creator>alexey.zh</dc:creator>
      <pubDate>Sun, 26 Apr 2026 09:58:48 +0000</pubDate>
      <link>https://dev.to/alzhi_f93e67fa45b972/web-ui-in-go-nothing-can-stop-me-2j7k</link>
      <guid>https://dev.to/alzhi_f93e67fa45b972/web-ui-in-go-nothing-can-stop-me-2j7k</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Author's note: This article is not technical in nature.&lt;br&gt;
Rather, it is a set of reflections and a fragmented stream of thoughts, something to read on the subway on the way to work.&lt;br&gt;
It should not be taken too seriously.&lt;/p&gt;

&lt;p&gt;Let's begin.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev.to/alzhi_f93e67fa45b972/a-long-story-about-how-i-dug-into-the-postgresql-source-code-to-write-my-own-wal-receiver-and-what-1648"&gt;In the previous part&lt;/a&gt;&lt;br&gt;
I briefly described my adventures while developing a &lt;a href="https://github.com/pgrwl/pgrwl" rel="noopener noreferrer"&gt;WAL receiver&lt;/a&gt;.&lt;br&gt;
Now it is time to continue.&lt;br&gt;
Not because I need it, not because anyone else needs it.&lt;br&gt;
Some things simply happen on their own, like you were just asleep a moment ago, and now you are already writing notes in the margins.&lt;/p&gt;

&lt;p&gt;It all started with a timid internal question.&lt;/p&gt;

&lt;p&gt;What if I attach the simplest possible web panel to the application? Sounds like an excellent idea.&lt;/p&gt;

&lt;p&gt;Exactly until the moment you remember one small detail: you do not know how to write frontend at all. Not at all.&lt;/p&gt;

&lt;p&gt;That is, at the level of ideas - yes, of course, now we will quickly make a neat web face, a couple of pages, a nice status view, tables, buttons. In practice, however, it looked roughly like this:&lt;br&gt;
"Oh, cool idea."&lt;br&gt;
Five minutes later:&lt;br&gt;
"Wait... but the last time I touched frontend was who knows when."&lt;/p&gt;

&lt;p&gt;But from a certain point, the idea became not just good, but truly tempting. Because if a task does not contain at least a drop of adventurism, self-deception, and light engineering recklessness, is it even really my project?&lt;/p&gt;

&lt;h2&gt;
  
  
  What Skill Loss Is
&lt;/h2&gt;

&lt;p&gt;A couple of years ago, I already had a period when I studied frontend. And, to my surprise, I even liked it back then.&lt;br&gt;
Specifically, Angular, the second one, the one on TypeScript.&lt;/p&gt;

&lt;p&gt;Why Angular?&lt;br&gt;
Because at that time, I was working with Java, and Angular seemed suspiciously similar in structure.&lt;br&gt;
Understandable entities - services, components, dependency injection - all of this at least resembled not shamanism, but some kind of system. Yes, on top of it all, there was a generous sprinkling of HTML, CSS, and other frontend rituals, but in general, there was no feeling that you were picking at an ancient altar assembled from npm packages and developers' tears.&lt;/p&gt;

&lt;p&gt;That is, back then, frontend did not look like hostile territory, but rather like a strange but tolerable neighbor.&lt;/p&gt;

&lt;p&gt;But the years passed, and all that knowledge evaporated so thoroughly as if it had never existed.&lt;br&gt;
Not "I kind of forgot it", not "I need to refresh it", but precisely disappeared. Evaporated. Erased. Drowned somewhere between work tasks, Go, infrastructure, Postgres, Kubernetes, and other things that do not require discussing which state manager is fashionable today.&lt;/p&gt;

&lt;p&gt;When I tried to look toward modern frontend again, gloom caught up with me rather quickly. And not some noble gloom, but the ordinary,&lt;br&gt;
everyday kind: when you look at yet another stack and realize that just to start, you first need to reread half the internet, then install a thousand dependencies, then understand why it does not build, and then also find out that all of it already became obsolete yesterday.&lt;/p&gt;

&lt;p&gt;And at that moment, it became clear: no, I am not climbing back into this from scratch. Life is one, and node_modules is unfortunately much larger.&lt;/p&gt;

&lt;p&gt;With that thought, I successfully put the idea aside for a year, threw it onto the shelf to gather dust together with other intentions sprinkled with laziness.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sometimes the Solution to Forgotten Problems Happens by Itself
&lt;/h2&gt;

&lt;p&gt;One day, while reading some random projects on GitHub, I stumbled upon htmx paired with Go. After digging further, I realized that this is a perfectly workable pattern.&lt;/p&gt;

&lt;p&gt;And then that rare feeling happened, when, instead of irritation, there is almost childlike joy. Suddenly, it turned out that you can avoid arranging a second higher education for yourself in SPA magic and make everything much simpler.&lt;/p&gt;

&lt;p&gt;In pure Go, with templates, with functions.&lt;br&gt;
Without a giant frontend stack.&lt;br&gt;
Without worshiping "reactivity".&lt;br&gt;
Without the feeling that, for the sake of two status pages, you are obliged to build a small copy of the modern web industry.&lt;/p&gt;

&lt;p&gt;I immediately felt like Eric Cartman -&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;[breathless] Mom-Mom! I've only just heard.&lt;br&gt;
They're making Chinpokomon dolls, mom.&lt;br&gt;
You can collect them all.&lt;br&gt;
You can collect them all, Mother; quick, come on. Let's go to the toy store!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Forward, this needs to be done quickly!&lt;/p&gt;

&lt;h3&gt;
  
  
  Planning and Implementation
&lt;/h3&gt;

&lt;p&gt;That is, in essence, you can build a completely normal single-page application - not in the sense of "a revolutionary interface of the future", but in the sense of a normal, honest, utilitarian status page. One that does not try to impress investors, but simply shows you what is going on in the system, and preferably without unnecessary complications.&lt;/p&gt;

&lt;p&gt;After that, the idea suddenly stopped being nonsense and turned into a plan. A dangerous symptom, by the way.&lt;br&gt;
This is exactly how the story of any side project usually begins:&lt;br&gt;
"I'll just try a little."&lt;br&gt;
And then you look - you already have architecture, API, UI, separate models, and for some reason you are seriously discussing the convenience of switching between receivers.&lt;/p&gt;

&lt;p&gt;Then everything went according to the classics of handcrafted engineering creativity. I quickly sketched out&lt;br&gt;
what it should roughly look like, wandered around websites, peeked at other people's templates, assembled a general style in my head, and began drawing a mockup.&lt;/p&gt;

&lt;p&gt;The old-fashioned way - on paper.&lt;/p&gt;

&lt;p&gt;Yes, literally on paper.&lt;br&gt;
Not in Figma, not in Sketch, not in anything else fashionable and respectable. Just a sheet of paper, a pen, rectangles, labels, arrows. Like a person who is not fighting&lt;br&gt;
for the title of "frontend developer of the year", but simply wants to understand where the WAL files will be and where the refresh button will be.&lt;/p&gt;

&lt;p&gt;And, honestly, there is even a certain charm in this.&lt;br&gt;
When you have no claim to "world-class product design",&lt;br&gt;
it is surprisingly easy to focus on what is actually needed.&lt;/p&gt;

&lt;p&gt;And what is needed, in fact, is quite simple.&lt;/p&gt;

&lt;p&gt;First, such a panel really helps with debugging.&lt;br&gt;
You do not have to climb into the container every time,&lt;br&gt;
wander through directories, run yet another ls, find, cat, curl, and pretend that this is normal UX for a living human being.&lt;br&gt;
You open the page, and there in front of your eyes are WAL files, backups, configuration, and overall status. Everything in one place.&lt;/p&gt;

&lt;p&gt;Besides, an important part is that you can run a check for the existence of a backup, take its end LSN, select all WAL files, make sure that none of them are missing, and that recovery is possible. Next in the plans is to make something like a recovery verification as an option.&lt;/p&gt;

&lt;p&gt;Not that this is merely an option; rather, it is the most important thing in the process. &lt;strong&gt;If a backup is not verified, then consider that you do not have a backup at all.&lt;/strong&gt;&lt;br&gt;
But as a rule, such checks are performed manually (I do it myself too, sometimes not too often because of laziness).&lt;/p&gt;

&lt;p&gt;And with an interface and the possibility of automation, there will also be a desire to perform checks more often; besides, it is not just interesting, it adds peace of mind.&lt;br&gt;
Beauty? Well, maybe not beauty, but not archaeology either.&lt;/p&gt;

&lt;p&gt;Second, you can switch between receivers.&lt;br&gt;
Because, of course, there may be several of them.&lt;/p&gt;

&lt;p&gt;Initially, of course, I did not plan to do this.&lt;br&gt;
At all. Not because the idea is bad, but because it seemed like one could live without it anyway. The console suits me 500%.&lt;br&gt;
Plus, any website immediately causes natural suspicions: now there will be polling, background requests, endless updates, meaningless HTTP load, and in the end, you will attach a small source of indignation to yourself.&lt;/p&gt;

&lt;p&gt;That is, first you make a panel "for convenience", and then you catch yourself realizing that the panel itself has become the thing that needs to be watched. Very engineering-like.&lt;br&gt;
Tests for tests.&lt;/p&gt;

&lt;p&gt;But then I thought: what if I do not turn it into a hysterical television with constant flickering?&lt;br&gt;
What if I abandon endless polling loops and automatic observation altogether, and leave only manual refresh?&lt;/p&gt;

&lt;p&gt;And suddenly everything started to look very reasonable.&lt;/p&gt;

&lt;p&gt;Because in this form it is, in essence, the same curl request - only in human form. No watching-cycle, no background fuss, no unnecessary magic. Wanted to - opened it. Wanted to - refreshed it.&lt;br&gt;
Looked at the current state, got the information you needed, and moved on.&lt;/p&gt;

&lt;p&gt;That is, without SPA, without multilayeredness, without assembling a universe for the sake of a couple of tables.&lt;br&gt;
Without the feeling that you accidentally subscribed to a second project instead of making a small improvement.&lt;/p&gt;

&lt;p&gt;Just Go, templates, htmx, and a bit of adventurism.&lt;br&gt;
And sometimes, oddly enough, that is quite enough.&lt;/p&gt;

&lt;p&gt;Closer to the fifth version, the implementation began to resemble what I had drawn in my imagination. Of course, smart people would have done it more correctly, while putting in less effort, with a more reasonable structure, using the right tool.&lt;/p&gt;

&lt;p&gt;But - sometimes a hammer, a stick, and swearing work wonders.&lt;/p&gt;

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

&lt;p&gt;The dashboard started working, release 0.1.0 is ready to start using it.&lt;br&gt;
From here on, it can be improved and polished.&lt;br&gt;
Since this is a fully optional component that is not connected to the main application in any way (although the sources do live in the root of the project), it is possible to plan separate levels of development.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffd8jcokvmgysmp0c1rcl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffd8jcokvmgysmp0c1rcl.png" alt=" " width="800" height="515"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Repository: &lt;a href="https://github.com/pgrwl/pgrwl" rel="noopener noreferrer"&gt;https://github.com/pgrwl/pgrwl&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Thank you for reading to the end!&lt;/p&gt;

</description>
      <category>go</category>
      <category>postgres</category>
    </item>
    <item>
      <title>A Long Story about how I dug into the PostgreSQL source code to write my own WAL receiver, and what came out of it</title>
      <dc:creator>alexey.zh</dc:creator>
      <pubDate>Sat, 18 Apr 2026 03:36:23 +0000</pubDate>
      <link>https://dev.to/alzhi_f93e67fa45b972/a-long-story-about-how-i-dug-into-the-postgresql-source-code-to-write-my-own-wal-receiver-and-what-1648</link>
      <guid>https://dev.to/alzhi_f93e67fa45b972/a-long-story-about-how-i-dug-into-the-postgresql-source-code-to-write-my-own-wal-receiver-and-what-1648</guid>
      <description>&lt;p&gt;Some thoughts are unpredictable.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;strong&gt;"I wonder how &lt;code&gt;pg_receivewal&lt;/code&gt; works internally?"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;From the outside, it sounds almost innocent. Really, what could possibly be wrong with that? Just ordinary engineering curiosity. I will take a quick look,&lt;br&gt;
understand the general structure, satisfy my curiosity, and then go on living peacefully.&lt;/p&gt;

&lt;p&gt;But then, for some reason, this happens:&lt;br&gt;
you are already building PostgreSQL from source, digging into &lt;code&gt;receivelog.c&lt;/code&gt;, comparing the behavior of your little creation with the original step by&lt;br&gt;
step, arguing with &lt;code&gt;fsync&lt;/code&gt;, looking at &lt;code&gt;.partial&lt;/code&gt; files like old friends, and suddenly discovering that you are writing&lt;br&gt;
your own &lt;a href="https://github.com/pgrwl/pgrwl" rel="noopener noreferrer"&gt;WAL receiver&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;In short, everything started quite normally and with absolutely no signs of anything serious.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why PostgreSQL in the First Place
&lt;/h2&gt;

&lt;p&gt;I have been using PostgreSQL as the main DBMS in almost all of my projects for a long time - both personal and work-related. And the longer you&lt;br&gt;
work with it, the more clearly you understand: this is not just a "good database". This is a system designed by people with a very&lt;br&gt;
serious engineering culture.&lt;/p&gt;

&lt;p&gt;When you read notes, discussions, and articles from PostgreSQL developers, you quickly notice how deeply they think through&lt;br&gt;
changes, trade-offs, new features, and behavior in complex scenarios. After such materials, I usually&lt;br&gt;
had a mixed feeling:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;admiration&lt;/li&gt;
&lt;li&gt;respect&lt;/li&gt;
&lt;li&gt;and a slight feeling that I had once again looked at work of a level unreachable for me&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;PostgreSQL gives you everything you need out of the box for backups and continuous WAL archiving. Including&lt;br&gt;
&lt;code&gt;pg_receivewal&lt;/code&gt; - the utility that eventually set everything in motion for me.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why Exactly &lt;code&gt;pg_receivewal&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Because it is a very good utility. And good utilities are especially dangerous: they make you want to understand exactly how they&lt;br&gt;
are built.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;pg_receivewal&lt;/code&gt; continuously receives WAL segments, can work in synchronous and asynchronous replication modes, and in general&lt;br&gt;
looks fairly straightforward. From a distance.&lt;/p&gt;

&lt;p&gt;Up close, it turns out that there are quite a few subtle things there:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;how the main loop starts&lt;/li&gt;
&lt;li&gt;how connection drops are survived&lt;/li&gt;
&lt;li&gt;how restart is performed&lt;/li&gt;
&lt;li&gt;at what point &lt;code&gt;.partial&lt;/code&gt; becomes a complete WAL file&lt;/li&gt;
&lt;li&gt;how timeline switching is handled&lt;/li&gt;
&lt;li&gt;where and when important &lt;code&gt;fsync&lt;/code&gt; calls must happen&lt;/li&gt;
&lt;li&gt;what to do so that it is reliable, not slow, and not embarrassing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So, as usual: a simple utility with a decent amount of engineering accuracy hidden around it.&lt;/p&gt;
&lt;h2&gt;
  
  
  A Few Words About Other Good Solutions I Looked at With Respect and Envy
&lt;/h2&gt;

&lt;p&gt;Before writing something of my own, of course, I spent a lot of time looking at already existing solutions.&lt;/p&gt;

&lt;p&gt;I use two of them at work for continuous archiving of the most critical and main databases.&lt;/p&gt;
&lt;h3&gt;
  
  
  &lt;code&gt;pgBackRest&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;pgBackRest&lt;/code&gt; is, without exaggeration, an engineering tank. Everything in its source code is impressive:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;logging&lt;/li&gt;
&lt;li&gt;testing&lt;/li&gt;
&lt;li&gt;architectural discipline&lt;/li&gt;
&lt;li&gt;incremental and differential backups&lt;/li&gt;
&lt;li&gt;support for large installations&lt;/li&gt;
&lt;li&gt;attention to edge cases&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And, of course, validation by the community and by time.&lt;/p&gt;

&lt;p&gt;When you read the code of this tool, you catch yourself thinking: yes, this is what a product&lt;br&gt;
written by people who know what they are doing looks like.&lt;br&gt;
And then you open your own repository and immediately become humble.&lt;/p&gt;
&lt;h3&gt;
  
  
  &lt;code&gt;Barman&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;I like Barman for a different reason.&lt;br&gt;
It does not try to magically solve everything in the world.&lt;br&gt;
It is, essentially, a very understandable orchestrator around standard PostgreSQL tools: &lt;code&gt;pg_receivewal&lt;/code&gt; and &lt;code&gt;pg_basebackup&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It has a quality that I value a lot: &lt;strong&gt;a simple and reliable model&lt;/strong&gt;.&lt;br&gt;
Not "everything at once", but careful automation around already existing, proven tools.&lt;/p&gt;

&lt;p&gt;This also strongly influenced how I started thinking about my own tool.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why Go, If I Had to Look at So Much C
&lt;/h2&gt;

&lt;p&gt;I decided to write my tool in Go.&lt;/p&gt;

&lt;p&gt;The reasons are fairly ordinary:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;recently, I have really enjoyed writing in this concise language&lt;/li&gt;
&lt;li&gt;simplicity and a UNIX background&lt;/li&gt;
&lt;li&gt;it is convenient for writing network and system-level things&lt;/li&gt;
&lt;li&gt;concurrency is handled well in it&lt;/li&gt;
&lt;li&gt;it fits cloud-native scenarios very naturally&lt;/li&gt;
&lt;li&gt;and, importantly, it is still a little harder to accidentally shoot yourself in the foot with a grenade launcher&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But there is an important nuance: to understand PostgreSQL, I had to seriously dig into C code.&lt;/p&gt;

&lt;p&gt;And here I want to separately say something I formulated for myself a long time ago:&lt;br&gt;
&lt;strong&gt;C is, in my opinion, both the most difficult and the most brilliant language at the same time.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I have not spent as much time on any other language trying to understand its semantics.&lt;br&gt;
Syntax is nothing - semantics are everything. Pointers alone are a simple concept, but&lt;br&gt;
hide a whole chain of icebergs underneath. There was even a time when I was making a compiler for C, with a preprocessor,&lt;br&gt;
assembler, and PE32 output (&lt;code&gt;*.exe&lt;/code&gt;). I played with that for a long time; it was a very interesting experience and time spent happily.&lt;/p&gt;

&lt;p&gt;The C language is so direct, so honest, and so close to the metal that it becomes scary. It feels like&lt;br&gt;
it is very easy to make six sextillion mistakes in it just while opening a file and taking a breath. One pointer going the wrong way -&lt;br&gt;
and that is it, hello, a new form of humiliation. Segmentation Fault becomes a kind of spell that must not be said out loud, lest you&lt;br&gt;
summon it.&lt;/p&gt;

&lt;p&gt;With all that said, I cannot say that I know C.&lt;br&gt;
Honestly, I probably know about three percent of it. And even that only on a good day.&lt;/p&gt;

&lt;p&gt;But even those three percent were extremely useful to me.&lt;br&gt;
Without them, I would not have been able to read PostgreSQL properly: to separate real logic from my own delusions,&lt;br&gt;
follow the control flow, and at least roughly understand why everything here is arranged this way and not another.&lt;/p&gt;

&lt;p&gt;So formally I wrote the tool in Go, but in practice this project also became my way of touching C a little more deeply&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;and gaining even more respect for the people who have been writing such systems in it for years.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  The Beginning: Compiling PostgreSQL, Debugging, and the First Signs of Recklessness
&lt;/h2&gt;

&lt;p&gt;To understand the implementation details at all, I had to go into the PostgreSQL source code.&lt;/p&gt;

&lt;p&gt;I had to learn how to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;build PostgreSQL from source&lt;/li&gt;
&lt;li&gt;run it in debug mode&lt;/li&gt;
&lt;li&gt;attach a debugger&lt;/li&gt;
&lt;li&gt;watch how calls flow&lt;/li&gt;
&lt;li&gt;understand what happens inside the replication loop&lt;/li&gt;
&lt;li&gt;establish the relationship between components and functions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And here I got a surprise: all of this turned out to be less scary than I had imagined. PostgreSQL built, &lt;code&gt;pg_receivewal&lt;/code&gt;&lt;br&gt;
started, the debugger attached to the process, and this immediately gave me the dangerous confidence that "well,&lt;br&gt;
now I will definitely figure this out quickly".&lt;/p&gt;

&lt;p&gt;Of course, I did not figure it out.&lt;/p&gt;

&lt;p&gt;The first thing I did was, like a true amateur, add the most aggressive tracing possible. I logged everything:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;function entries&lt;/li&gt;
&lt;li&gt;exits&lt;/li&gt;
&lt;li&gt;variable values&lt;/li&gt;
&lt;li&gt;branches&lt;/li&gt;
&lt;li&gt;important calls&lt;/li&gt;
&lt;li&gt;and sometimes, it seemed, the mere fact that the universe existed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At first, it seems very clever. Then you have gigantic logs, you no longer understand whether you are reading the system or whether it is slowly&lt;br&gt;
breaking your mind, and the realization comes: &lt;strong&gt;many logs do not mean much understanding&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;But at this stage, the overall picture started to emerge. I began to understand how entities are connected, where the WAL receiving&lt;br&gt;
loop starts, how errors are survived, what happens to &lt;code&gt;.partial&lt;/code&gt;, and at which moments decisions are made about completing a segment.&lt;br&gt;
I discovered libraries, very well-written and years-polished file handling functions, and many more insanely cool things for&lt;br&gt;
the piggy bank of my mind.&lt;/p&gt;

&lt;p&gt;And at some point I could not resist: enough watching, time to write.&lt;/p&gt;
&lt;h2&gt;
  
  
  The First Prototype: "I Will Just Reproduce &lt;code&gt;pg_receivewal&lt;/code&gt;"
&lt;/h2&gt;

&lt;p&gt;I had a very naive idea: not to invent anything new, but simply to reproduce the behavior of&lt;br&gt;
&lt;code&gt;pg_receivewal&lt;/code&gt; as closely as possible.&lt;/p&gt;

&lt;p&gt;In theory, it sounds wonderful.&lt;br&gt;
In practice, it means that you voluntarily sign up for weeks of studying:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;exactly how the streaming loop starts&lt;/li&gt;
&lt;li&gt;how it reacts to connection drops with the database&lt;/li&gt;
&lt;li&gt;what a correct restart should look like, from which file and from which offset inside it&lt;/li&gt;
&lt;li&gt;when a &lt;code&gt;.partial&lt;/code&gt; file can be considered complete&lt;/li&gt;
&lt;li&gt;how timeline changes are handled&lt;/li&gt;
&lt;li&gt;where you misunderstood something&lt;/li&gt;
&lt;li&gt;and where you no longer understand anything at all, but continue out of stubbornness&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;My first more-or-less stable prototype appeared after a couple of weeks. And those were very fun weeks. At times I&lt;br&gt;
felt like a researcher and a super-cool mega-hacker, at other times - like a person who crawled into an aircraft engine without a license to repair it&lt;br&gt;
using someone else's notes.&lt;/p&gt;

&lt;p&gt;But there is one thing I really want to point out: PostgreSQL code is surprisingly pleasant to read. Good comments, competent&lt;br&gt;
decomposition, respect for the reader and colleagues. Even if you yourself understand about twenty percent, it is still clear that in front of you is very&lt;br&gt;
strong engineering work.&lt;/p&gt;
&lt;h2&gt;
  
  
  When You Realize That Simply Receiving WAL Is Only the Beginning
&lt;/h2&gt;

&lt;p&gt;When the prototype finally worked, the joy did not last long.&lt;/p&gt;

&lt;p&gt;Because I already understood: &lt;strong&gt;receiving WAL is only half the job&lt;/strong&gt;. And then the usual engineering carnival begins:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;compression&lt;/li&gt;
&lt;li&gt;encryption&lt;/li&gt;
&lt;li&gt;uploading to S3&lt;/li&gt;
&lt;li&gt;uploading to SFTP&lt;/li&gt;
&lt;li&gt;cleaning up old files&lt;/li&gt;
&lt;li&gt;monitoring&lt;/li&gt;
&lt;li&gt;external scripts&lt;/li&gt;
&lt;li&gt;cron&lt;/li&gt;
&lt;li&gt;more scripts&lt;/li&gt;
&lt;li&gt;and then scripts that fix the previous scripts&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And I have never liked this universe of external glue. Because it almost always looks like it was written&lt;br&gt;
at night under the threat of a production incident, and then everyone was afraid to touch it. And all of it smells bad and looks disgusting.&lt;/p&gt;

&lt;p&gt;Scripts around WAL archiving are often fragile, non-obvious, poorly tested, and live on faith that "it somehow&lt;br&gt;
works". And in critical things, I wanted exactly the opposite.&lt;/p&gt;

&lt;p&gt;I wanted the main program itself to manage the archive:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;to know what can already be compressed&lt;/li&gt;
&lt;li&gt;to know what still cannot be deleted&lt;/li&gt;
&lt;li&gt;to understand when a file can be sent to remote storage&lt;/li&gt;
&lt;li&gt;and not to try to make such decisions through a layer of suspicious bash magic&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So management components began to appear around the WAL receiver:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;one receives the log&lt;/li&gt;
&lt;li&gt;another archives and encrypts&lt;/li&gt;
&lt;li&gt;a third sends files to S3 or SFTP&lt;/li&gt;
&lt;li&gt;a fourth handles retention and automatic cleanup&lt;/li&gt;
&lt;li&gt;a fifth collects metrics and monitors process state&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And at that point, the project stopped being "just a utility". It started turning into a small system where coordination,&lt;br&gt;
order, and the absence of internal fights between components mattered.&lt;/p&gt;
&lt;h2&gt;
  
  
  About Base Backup: I Did Not Want To, but Curiosity Won
&lt;/h2&gt;

&lt;p&gt;Initially, I had no intention of implementing base backup at all.&lt;/p&gt;

&lt;p&gt;The reason is simple: the replication protocol is single-threaded. For small databases, that is fine. For large ones - not so rosy anymore.&lt;br&gt;
If a backup takes ten hours every ten hours, that is, to put it mildly, not always convenient.&lt;/p&gt;

&lt;p&gt;Multi-threaded approaches usually require the tool to live next to the database itself. And I wanted exactly the opposite: to remotely&lt;br&gt;
collect WAL and make backups from databases located anywhere - in the cloud, on virtual machines, in Kubernetes - and at the same time not&lt;br&gt;
require sidecar containers or any special infrastructure changes from them.&lt;/p&gt;

&lt;p&gt;But then the thing that happens to many technical projects happened:&lt;br&gt;
I did not plan this functionality, and then it simply became interesting.&lt;/p&gt;

&lt;p&gt;In the end, I did implement streaming base backup. It does not claim to be a universal solution for huge&lt;br&gt;
installations, but for databases around 200 GiB it turned out to be quite practical. A couple of hours for a nightly job is already a reasonable&lt;br&gt;
scenario.&lt;/p&gt;

&lt;p&gt;So it turned out not to be a "superweapon", but an honest working tool in a clear niche.&lt;/p&gt;
&lt;h3&gt;
  
  
  Why I Did Not Go Deeper Into Incremental Backups
&lt;/h3&gt;

&lt;p&gt;Of course, I also looked at incremental / differential backups.&lt;/p&gt;

&lt;p&gt;But there you quickly understand an unpleasant thing: taking an incremental backup is not victory yet. You then have to&lt;br&gt;
assemble it back correctly. And that means a completely different level of complexity begins:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;either write your own analogue of &lt;code&gt;pg_combinebackup&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;or very carefully depend on an external tool&lt;/li&gt;
&lt;li&gt;or drown in the number of edge cases and incompatibilities&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At that point I honestly looked at the task and decided that I already had enough problems without it.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;pgBackRest&lt;/code&gt; does such things in a truly well-thought-out way. But reproducing that level is not "built over a couple of&lt;br&gt;
weekends on enthusiasm". It is large, heavy engineering work for years. So I consciously stopped at a simpler&lt;br&gt;
model: reliable base backup for small and medium production environments.&lt;/p&gt;

&lt;p&gt;Without claims to world domination. Just a working, predictable thing.&lt;/p&gt;
&lt;h2&gt;
  
  
  Architecture: The Moment When You Are No Longer Writing a Utility but Coordinating Chaos
&lt;/h2&gt;

&lt;p&gt;As soon as you have several background processes, it immediately becomes clear that the main difficulty is no longer WAL as&lt;br&gt;
such, but making sure this whole household does not fight with itself.&lt;/p&gt;

&lt;p&gt;You need to be able to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;not start a backup if another one has not finished yet&lt;/li&gt;
&lt;li&gt;not start archiving if it is already running&lt;/li&gt;
&lt;li&gt;not delete something that may still be needed&lt;/li&gt;
&lt;li&gt;handle errors correctly&lt;/li&gt;
&lt;li&gt;carefully stop background processes&lt;/li&gt;
&lt;li&gt;keep the system in a predictable state&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here I had to seriously think about patterns:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;job queue&lt;/li&gt;
&lt;li&gt;worker pool&lt;/li&gt;
&lt;li&gt;supervisor&lt;/li&gt;
&lt;li&gt;pipes&lt;/li&gt;
&lt;li&gt;task lifecycle management&lt;/li&gt;
&lt;li&gt;safe shutdown&lt;/li&gt;
&lt;li&gt;goroutine coordination&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At some point I realized that I was no longer "writing a WAL receiver". I was assembling a gearbox. And if even one gear&lt;br&gt;
shifts a little, all of this will either start screaming or silently break. And silently breaking software is the worst kind of software.&lt;br&gt;
At the same time, the main task was to make sure the main WAL receiving process was not affected by "noisy neighbors".&lt;/p&gt;
&lt;h2&gt;
  
  
  Streaming Large Files: Another Source of Creativity
&lt;/h2&gt;

&lt;p&gt;There is another pleasant task as well: transferring large backup files to remote storage.&lt;/p&gt;

&lt;p&gt;When a database weighs, for example, 300 GiB, you quickly understand:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;you do not want to save everything locally, and often it is not convenient&lt;/li&gt;
&lt;li&gt;you cannot pull it all into memory&lt;/li&gt;
&lt;li&gt;you also do not want to write a crooked intermediate scheme, because you will have to maintain it yourself later&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So you need a proper streaming pipeline: read the data, transform it on the way, and immediately send it further - without&lt;br&gt;
intermediate garbage, without extra storage, without special effects.&lt;/p&gt;

&lt;p&gt;Here Go was useful again. It has good primitives for streaming processing. Although the presence of primitives, of course, does not&lt;br&gt;
stop you from making design mistakes for a very long time.&lt;/p&gt;
&lt;h2&gt;
  
  
  &lt;code&gt;fsync&lt;/code&gt;: The Most Subtle Part and My Own Little Nervous Breakdown
&lt;/h2&gt;

&lt;p&gt;If I had to choose what drained the most blood from me, the winner is obvious: &lt;code&gt;fsync&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This is the place where you first think: "well, this part is simple". And then you discover that you have been staring at&lt;br&gt;
the &lt;code&gt;receivelog.c&lt;/code&gt; source code for several hours with the expression of a person who has voluntarily entered a very strange stage of life.&lt;/p&gt;

&lt;p&gt;The problem here is that it is easy to be wrong in both directions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;call &lt;code&gt;fsync&lt;/code&gt; too often - everything slows down&lt;/li&gt;
&lt;li&gt;call it too rarely - later you may look at the result very sadly&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So it is either slow or shameful. Quite a rich choice, to put it mildly.&lt;/p&gt;

&lt;p&gt;I had to literally compare the behavior of my implementation with &lt;code&gt;pg_receivewal&lt;/code&gt; step by step:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;where exactly synchronization happens&lt;/li&gt;
&lt;li&gt;at what moment&lt;/li&gt;
&lt;li&gt;why exactly there&lt;/li&gt;
&lt;li&gt;which scenarios must force &lt;code&gt;fsync&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;and how to do neither too much nor too little&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In the end, the key points turned out to be:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;fsync&lt;/code&gt; after finishing writing a segment&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;fsync&lt;/code&gt; when renaming &lt;code&gt;.partial&lt;/code&gt; to the final WAL file&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;fsync&lt;/code&gt; on keepalive if the server requests a reply&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;fsync&lt;/code&gt; on errors in the receiving loop&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then the truly fun part began: integration checks. I ran two receivers simultaneously (&lt;code&gt;pg_receivewal&lt;/code&gt;, &lt;code&gt;pgrwl&lt;/code&gt;), generated&lt;br&gt;
WAL, compared timings, then compared the resulting files byte by byte, measured timing differences in milliseconds, and tried to remove&lt;br&gt;
everything unnecessary.&lt;/p&gt;

&lt;p&gt;I even got to logging: in places like this, you begin to understand that it can be either a helper or a quiet&lt;br&gt;
saboteur. For example, you do not need to parse attributes if the logging level does not require it; extra CPU cycles&lt;br&gt;
can be spent on more useful things.&lt;/p&gt;

&lt;p&gt;In the end, I managed to achieve very similar behavior and complete matching of the resulting WAL files over the same interval. And&lt;br&gt;
the small timing difference remained only where it is normal: two daemons cannot be started in the exact same&lt;br&gt;
physical microsecond, no matter how hard you try.&lt;/p&gt;

&lt;p&gt;In the fight against slowness, I even quickly wrote a small utility that injects&lt;br&gt;
a &lt;code&gt;defer&lt;/code&gt; into EVERY function, where the runtime of that function is measured. Not the best check,&lt;br&gt;
but, as practice showed, it helps quickly identify especially hot functions, and then point&lt;br&gt;
the profiler, debugger, and so on at them. My tracing looks something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;FUNCTION                            CALLS  TOTAL_NS     TOTAL_SEC
--------                            -----  --------     ---------
storecrypt.Put                      70     23061361400  23.06
receivesuperv.uploadOneFile         35     11606918000  11.61
fsync.Fsync                         106    8813968000   8.81
xlog.processOneMsg                  4481   6818721600   6.82
xlog.processXLogDataMsg             4481   6814495400   6.81
xlog.CloseWalFile                   35     6561511500   6.56
xlog.closeAndRename                 35     6559979000   6.56
fsync.FsyncFname                    70     6525596900   6.53

.....500 more lines
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Metrics: Because I Wanted to See Whether It Was Still Alive or Already Dead
&lt;/h2&gt;

&lt;p&gt;Over time, I also added metrics:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;number of files&lt;/li&gt;
&lt;li&gt;archive size&lt;/li&gt;
&lt;li&gt;number of errors&lt;/li&gt;
&lt;li&gt;transferred bytes&lt;/li&gt;
&lt;li&gt;state of background tasks&lt;/li&gt;
&lt;li&gt;deleted files&lt;/li&gt;
&lt;li&gt;general runtime statistics&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I even made a Grafana dashboard. Not the most beautiful one in the world, but useful enough to quickly understand: everything is still&lt;br&gt;
alive or it is already time to get nervous.&lt;/p&gt;

&lt;p&gt;It was important to me to make metrics free if they are disabled. So wherever possible, I used the&lt;br&gt;
noop approach: if observability is not needed, the system should not pay for it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Logging: Where I Also Realized I Still Have a Long Way to Go
&lt;/h2&gt;

&lt;p&gt;Logging had its own coming-of-age story.&lt;/p&gt;

&lt;p&gt;At first, I logged everything. Because, as everyone knows, any person who has deeply entered a complex system for the first time&lt;br&gt;
starts with the phrase: "I will just add more logs and understand everything".&lt;/p&gt;

&lt;p&gt;No.&lt;/p&gt;

&lt;p&gt;Many logs are not understanding. They are just many logs.&lt;/p&gt;

&lt;p&gt;Good logging is when, at the moment of a problem, logs really help you understand what is going on, and do not turn into&lt;br&gt;
an additional source of noise and despair.&lt;/p&gt;

&lt;p&gt;I have not yet managed to make this part as good as I would like. The current result is normal, but&lt;br&gt;
not exemplary. And in this sense, &lt;code&gt;pgBackRest&lt;/code&gt; still remains for me an example of a very smart and thoughtful approach: you can see&lt;br&gt;
how much discipline and engineering care went specifically into diagnostics.&lt;/p&gt;

&lt;h2&gt;
  
  
  Integration Tests: The Hardest and Most Important Part
&lt;/h2&gt;

&lt;p&gt;One of the most difficult and at the same time most necessary parts of the whole project is integration testing.&lt;/p&gt;

&lt;p&gt;Because a daemon that depends on another daemon is already not the easiest object to test. And if you&lt;br&gt;
also want to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;start PostgreSQL&lt;/li&gt;
&lt;li&gt;generate WAL&lt;/li&gt;
&lt;li&gt;stop processes&lt;/li&gt;
&lt;li&gt;make a backup&lt;/li&gt;
&lt;li&gt;restore the database&lt;/li&gt;
&lt;li&gt;compare the state before and after&lt;/li&gt;
&lt;li&gt;run failure scenarios&lt;/li&gt;
&lt;li&gt;check compatibility and correctness&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;then life starts playing in especially bright colors&lt;/p&gt;

&lt;p&gt;I settled on this approach: simple shell scripts that start the test environment in a container,&lt;br&gt;
populate the database, perform actions, then restore everything and check the result.&lt;br&gt;
I also really did not want to drag a ton of dependencies like testcontainers into the project.&lt;/p&gt;

&lt;p&gt;In the end, it turned out like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;shell scripts&lt;/li&gt;
&lt;li&gt;docker compose&lt;/li&gt;
&lt;li&gt;matrix in GitHub Actions&lt;/li&gt;
&lt;li&gt;isolated scenarios&lt;/li&gt;
&lt;li&gt;without unnecessary heavy magic where understandable mechanics are enough&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is how I got tests for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;comparison with &lt;code&gt;pg_receivewal&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;backup/restore&lt;/li&gt;
&lt;li&gt;uploading to S3 and SFTP&lt;/li&gt;
&lt;li&gt;correctness of WAL files&lt;/li&gt;
&lt;li&gt;stopping and restarting&lt;/li&gt;
&lt;li&gt;different failure scenarios&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And honestly, integration tests are what give me the main confidence in releases. Not one hundred percent, of course. One hundred&lt;br&gt;
percent in such things is promised either by madmen or by marketers. But good, engineering-honest confidence - yes.&lt;/p&gt;

&lt;p&gt;Unit tests, of course, also exist. But for me, integration checks are the main criterion&lt;br&gt;
that all of this is not only nicely written (not nicely everywhere), but actually works.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Came Out of It
&lt;/h2&gt;

&lt;p&gt;Over time, from the fairly harmless desire to "just see how &lt;code&gt;pg_receivewal&lt;/code&gt; works", a tool grew that now has:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;streaming WAL receiver&lt;/li&gt;
&lt;li&gt;archiving&lt;/li&gt;
&lt;li&gt;compression&lt;/li&gt;
&lt;li&gt;encryption (streaming AES-256-GCM)&lt;/li&gt;
&lt;li&gt;uploading to S3 (streaming, +multipart)&lt;/li&gt;
&lt;li&gt;uploading to SFTP&lt;/li&gt;
&lt;li&gt;retention and automatic cleanup&lt;/li&gt;
&lt;li&gt;metrics&lt;/li&gt;
&lt;li&gt;logging (mostly zero-cost)&lt;/li&gt;
&lt;li&gt;base backup&lt;/li&gt;
&lt;li&gt;configuration through a file and environment variables&lt;/li&gt;
&lt;li&gt;controlled shutdown&lt;/li&gt;
&lt;li&gt;unit and integration tests&lt;/li&gt;
&lt;li&gt;behavior comparison with &lt;code&gt;pg_receivewal&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;documentation with diagrams and examples&lt;/li&gt;
&lt;li&gt;as many usage examples as possible (standalone/docker-compose/k8s)&lt;/li&gt;
&lt;li&gt;helm-chart (quite simple and working)&lt;/li&gt;
&lt;li&gt;website (in progress, but at least now it is clear how this is done and that it is possible)&lt;/li&gt;
&lt;li&gt;a set of patterns and libraries for further reuse in Go projects&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So, as usually happens, the project long ago stopped being what it seemed to be at the beginning.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Is Planned
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;improve metrics, remove what is unnecessary, add what is needed, build a truly useful and beautiful dashboard&lt;/li&gt;
&lt;li&gt;improve logging quality, make it consistent, think through levels more carefully, preserve zero-cost semantics&lt;/li&gt;
&lt;li&gt;add new capabilities for base backup - around fine-tuning retention periods&lt;/li&gt;
&lt;li&gt;a huge amount of space for refactoring and documentation&lt;/li&gt;
&lt;li&gt;add even more integration tests, I am planning a V2 version&lt;/li&gt;
&lt;li&gt;add every "breaking" scenario to the tests that my imagination can produce&lt;/li&gt;
&lt;li&gt;make the website properly, right now it is just a copy of the documentation&lt;/li&gt;
&lt;li&gt;create a user guide (because it is simply interesting)&lt;/li&gt;
&lt;li&gt;and much more&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What I Took Away From This
&lt;/h2&gt;

&lt;p&gt;Perhaps the main result is not that I wrote yet another tool.&lt;/p&gt;

&lt;p&gt;The main result is something else:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I understood PostgreSQL much more deeply&lt;/li&gt;
&lt;li&gt;I gained even more respect for C, although I know about a miserable three percent of it&lt;/li&gt;
&lt;li&gt;I saw how difficult it is to reproduce even a small part of the behavior of a well-made system utility&lt;/li&gt;
&lt;li&gt;and once again I became convinced that high-quality code written by others is the best way to quickly cure yourself of excessive
self-confidence&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Because one thing is to look at architecture from the outside and admire it.&lt;br&gt;
And it is a completely different thing to try to reproduce at least part of that logic yourself and not fall apart along the way.&lt;/p&gt;

&lt;p&gt;And yes. If it ever seems to you that the thought&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"maybe I should also write some utility for PostgreSQL?"&lt;/strong&gt;&lt;br&gt;
sounds like a good idea for a couple of quiet weekends -&lt;/p&gt;

&lt;p&gt;I have two pieces of news for you.&lt;/p&gt;

&lt;p&gt;The first: the idea really is interesting.&lt;br&gt;
The second: you most likely will not have quiet weekends anymore.&lt;/p&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.postgresql.org/docs/current/app-pgrwl.html" rel="noopener noreferrer"&gt;pg_receivewal Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/postgres/postgres/blob/master/src/bin/pg_basebackup/pg_receivewal.c" rel="noopener noreferrer"&gt;pg_receivewal Source Code&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.postgresql.org/docs/current/protocol-replication.html" rel="noopener noreferrer"&gt;Streaming Replication Protocol&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.postgresql.org/docs/current/continuous-archiving.html" rel="noopener noreferrer"&gt;Continuous Archiving and Point-in-Time Recovery&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.postgresql.org/docs/current/continuous-archiving.html#BACKUP-ARCHIVING-WAL" rel="noopener noreferrer"&gt;Setting Up WAL Archiving&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pgbackrest.org/" rel="noopener noreferrer"&gt;pgBackRest&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pgbarman.org/" rel="noopener noreferrer"&gt;Barman&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Repository: &lt;a href="https://github.com/pgrwl/pgrwl" rel="noopener noreferrer"&gt;https://github.com/pgrwl/pgrwl&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Thanks for reading!&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>go</category>
    </item>
    <item>
      <title>SQL-First PostgreSQL Migrations Without the Magic</title>
      <dc:creator>alexey.zh</dc:creator>
      <pubDate>Sun, 12 Apr 2026 14:29:11 +0000</pubDate>
      <link>https://dev.to/alzhi_f93e67fa45b972/sql-first-postgresql-migrations-without-the-magic-22b0</link>
      <guid>https://dev.to/alzhi_f93e67fa45b972/sql-first-postgresql-migrations-without-the-magic-22b0</guid>
      <description>&lt;p&gt;If you work with PostgreSQL long enough, you start noticing a pattern: migration tools often become more complicated than the schema changes they are supposed to manage.&lt;/p&gt;

&lt;p&gt;Some tools invent their own DSL.&lt;br&gt;
Some hide behavior in config files.&lt;br&gt;
Some couple migrations to an ORM.&lt;br&gt;
Some force a directory layout that looks neat in a demo but awkward in a real project.&lt;/p&gt;

&lt;p&gt;And then there is the simpler question:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why can’t PostgreSQL migrations just stay plain SQL?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That is the idea behind &lt;strong&gt;&lt;a href="https://github.com/hashmap-kz/gopgmigrate" rel="noopener noreferrer"&gt;gopgmigrate&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;It is a SQL-first migration tool for PostgreSQL that keeps the core workflow boring in the best possible way:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;write normal &lt;code&gt;.sql&lt;/code&gt; files&lt;/li&gt;
&lt;li&gt;organize them however you want&lt;/li&gt;
&lt;li&gt;run them in order&lt;/li&gt;
&lt;li&gt;track what was applied&lt;/li&gt;
&lt;li&gt;support rollbacks&lt;/li&gt;
&lt;li&gt;support repeatable migrations&lt;/li&gt;
&lt;li&gt;make non-transactional migrations explicit&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No YAML. No hidden DSL. No ORM lock-in. No magic comments.&lt;/p&gt;

&lt;p&gt;Just SQL files and a clear naming convention.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why this approach matters
&lt;/h2&gt;

&lt;p&gt;A migration file should be easy to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;read in a code review&lt;/li&gt;
&lt;li&gt;open in your editor&lt;/li&gt;
&lt;li&gt;run directly with &lt;code&gt;psql&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;troubleshoot at 2 AM&lt;/li&gt;
&lt;li&gt;keep using even if you stop using the tool&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last point matters more than many teams realize.&lt;/p&gt;

&lt;p&gt;A good migration format should outlive the tool that executes it. Your schema history is long-term infrastructure. It should not depend on a framework-specific abstraction that becomes painful to migrate away from later.&lt;/p&gt;

&lt;p&gt;With &lt;strong&gt;gopgmigrate&lt;/strong&gt;, the migration files remain usable as ordinary SQL. The tool adds safety and structure on top, but it does not take ownership of your database change process.&lt;/p&gt;
&lt;h2&gt;
  
  
  What gopgmigrate does
&lt;/h2&gt;

&lt;p&gt;At a high level, the workflow is simple:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Scan a directory tree recursively for SQL migration files&lt;/li&gt;
&lt;li&gt;Sort them globally by revision&lt;/li&gt;
&lt;li&gt;Compare them with the migration history stored in PostgreSQL&lt;/li&gt;
&lt;li&gt;Apply only what is pending&lt;/li&gt;
&lt;li&gt;Record hashes and metadata for auditability&lt;/li&gt;
&lt;li&gt;Support rolling back the last applied migrations&lt;/li&gt;
&lt;li&gt;Re-run repeatable scripts only when their content changes&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That gives you a clean PostgreSQL migration workflow with a small mental model.&lt;/p&gt;
&lt;h2&gt;
  
  
  The naming convention is the API
&lt;/h2&gt;

&lt;p&gt;One of the nicest design choices in gopgmigrate is that the file name itself declares the migration behavior.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcfqb5lsh6ang4np4l2d3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcfqb5lsh6ang4np4l2d3.png" alt=" " width="800" height="489"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;0000001-create-users-table.up.sql
0000001-create-users-table.down.sql
0000003-fn-get-users.r.up.sql
0000004-vacuum-users.notx.up.sql
0000005-refresh-stats.rnotx.up.sql
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is refreshingly explicit.&lt;/p&gt;

&lt;h3&gt;
  
  
  Versioned migrations
&lt;/h3&gt;

&lt;p&gt;These run once in order:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;0000002-add-roles-table.up.sql
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Rollbacks
&lt;/h3&gt;

&lt;p&gt;Rollback files are separate and predictable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;0000002-add-roles-table.down.sql
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Repeatable migrations
&lt;/h3&gt;

&lt;p&gt;Useful for functions, views, triggers, or other SQL objects you may want to refresh when the file changes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;0000003-fn-get-users.r.up.sql
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Non-transactional migrations
&lt;/h3&gt;

&lt;p&gt;Some PostgreSQL operations cannot run inside a transaction, for example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;VACUUM&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;CREATE INDEX CONCURRENTLY&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;DROP INDEX CONCURRENTLY&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;some forms of &lt;code&gt;REINDEX&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ALTER SYSTEM&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those are made explicit in the file name:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;0000004-vacuum-users.notx.up.sql
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And if a migration is both repeatable and non-transactional:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;0000005-refresh-stats.rnotx.up.sql
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a small detail, but it solves a real operational problem: the migration behavior is visible before you open the file.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real projects are not flat folders
&lt;/h2&gt;

&lt;p&gt;A lot of migration tools quietly assume every team wants the same directory structure.&lt;/p&gt;

&lt;p&gt;Reality is messier.&lt;/p&gt;

&lt;p&gt;Some teams want to split:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;schema&lt;/li&gt;
&lt;li&gt;data&lt;/li&gt;
&lt;li&gt;functions&lt;/li&gt;
&lt;li&gt;maintenance&lt;/li&gt;
&lt;li&gt;environment-specific files&lt;/li&gt;
&lt;li&gt;release-based groups&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is why I like that gopgmigrate does &lt;strong&gt;not&lt;/strong&gt; force a rigid directory layout.&lt;/p&gt;

&lt;p&gt;You can organize migrations by concern:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;migrations/
  schema/
  data/
  functions/
  no-transaction/
  down/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or by release:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;migrations/
  v1.0.0/
  v1.1.0/
  down/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or however your team naturally thinks about database changes.&lt;/p&gt;

&lt;p&gt;The only rule is that version ordering remains global.&lt;/p&gt;

&lt;p&gt;That is a practical compromise: freedom in layout, predictability in execution.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why SQL-first migrations are still the best default
&lt;/h2&gt;

&lt;p&gt;There is a reason SQL-first tools keep appealing to engineers who work close to PostgreSQL.&lt;/p&gt;

&lt;p&gt;PostgreSQL already has a powerful language for schema and data changes. It is called SQL.&lt;/p&gt;

&lt;p&gt;When a tool stays out of the way, you get a few concrete advantages:&lt;/p&gt;

&lt;h3&gt;
  
  
  Better reviewability
&lt;/h3&gt;

&lt;p&gt;A migration diff is just SQL. Reviewers do not have to mentally decode a framework abstraction.&lt;/p&gt;

&lt;h3&gt;
  
  
  Better portability
&lt;/h3&gt;

&lt;p&gt;You can run the file with &lt;code&gt;psql&lt;/code&gt;, a database IDE, automation scripts, or CI jobs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Better debugging
&lt;/h3&gt;

&lt;p&gt;When something fails, you are looking at the actual statement PostgreSQL rejected.&lt;/p&gt;

&lt;h3&gt;
  
  
  Better longevity
&lt;/h3&gt;

&lt;p&gt;Your migration history remains useful years later, even if your application stack changes.&lt;/p&gt;

&lt;p&gt;That makes SQL-first migration tooling especially attractive for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;platform teams&lt;/li&gt;
&lt;li&gt;backend teams with multiple services&lt;/li&gt;
&lt;li&gt;teams that avoid ORM-heavy workflows&lt;/li&gt;
&lt;li&gt;projects with long-lived PostgreSQL databases&lt;/li&gt;
&lt;li&gt;teams that want plain operational ownership&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Safety features that matter in practice
&lt;/h2&gt;

&lt;p&gt;Simple does not mean naive.&lt;/p&gt;

&lt;p&gt;For a migration tool to be usable in production, it needs a few guardrails. gopgmigrate includes some of the right ones:&lt;/p&gt;

&lt;h3&gt;
  
  
  Advisory locking
&lt;/h3&gt;

&lt;p&gt;This helps prevent concurrent migration runs from stepping on each other.&lt;/p&gt;

&lt;h3&gt;
  
  
  Transactional safety by default
&lt;/h3&gt;

&lt;p&gt;Most PostgreSQL DDL can run inside a transaction, and that is the safe default.&lt;/p&gt;

&lt;h3&gt;
  
  
  Explicit non-transactional mode
&lt;/h3&gt;

&lt;p&gt;Instead of hiding exceptions, the tool makes them obvious in the filename.&lt;/p&gt;

&lt;h3&gt;
  
  
  Hash-based change detection
&lt;/h3&gt;

&lt;p&gt;This is particularly useful for repeatable migrations. If the content changes, the tool knows it should re-apply the script.&lt;/p&gt;

&lt;h3&gt;
  
  
  History tracking
&lt;/h3&gt;

&lt;p&gt;Applied migrations are recorded in a history table, along with metadata such as hash and timing-related details.&lt;/p&gt;

&lt;p&gt;That is the kind of boring reliability you want from migration tooling.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example CLI workflow
&lt;/h2&gt;

&lt;p&gt;The CLI is intentionally straightforward.&lt;/p&gt;

&lt;p&gt;Apply pending migrations:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gopgmigrate migrate &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--dirname&lt;/span&gt; ./migrations &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--connstr&lt;/span&gt; postgres://user:pass@localhost:5432/mydb
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Preview without applying:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gopgmigrate migrate &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--dirname&lt;/span&gt; ./migrations &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--connstr&lt;/span&gt; postgres://user:pass@localhost:5432/mydb &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--dry-run&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rollback the last migration count:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gopgmigrate rollback-count 2 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--dirname&lt;/span&gt; ./migrations &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--connstr&lt;/span&gt; postgres://user:pass@localhost:5432/mydb
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use environment variables in CI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;PGMIGRATE_DIRNAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;./migrations
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;PGMIGRATE_CONNSTR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;postgres://user:pass@localhost:5432/mydb

gopgmigrate migrate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the kind of interface that works well in local development, CI pipelines, containerized jobs, and release automation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this fits especially well
&lt;/h2&gt;

&lt;p&gt;I think gopgmigrate is especially appealing in a few scenarios.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. PostgreSQL-first teams
&lt;/h3&gt;

&lt;p&gt;If your team understands PostgreSQL and prefers direct SQL over framework migration layers, this fits naturally.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Teams with mixed migration types
&lt;/h3&gt;

&lt;p&gt;Schema changes, data fixes, repeatable view/function refreshes, and non-transactional maintenance are all first-class cases here.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Repos with real structure
&lt;/h3&gt;

&lt;p&gt;If your migration directory stopped being a cute flat demo folder a long time ago, recursive scanning and flexible layouts are genuinely useful.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. CI/CD and automation
&lt;/h3&gt;

&lt;p&gt;The CLI is simple enough to drop into pipelines without teaching your delivery system a new configuration language.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Engineers who dislike lock-in
&lt;/h3&gt;

&lt;p&gt;Your migration files stay plain SQL. That is a strong long-term property.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I like most about this design
&lt;/h2&gt;

&lt;p&gt;The best tools often win not because they do more, but because they make fewer damaging decisions for you.&lt;/p&gt;

&lt;p&gt;gopgmigrate seems built around a healthy principle:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;the tool should manage execution, not redefine how SQL migrations ought to exist.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;your files remain readable&lt;/li&gt;
&lt;li&gt;your shell workflows still work&lt;/li&gt;
&lt;li&gt;your database knowledge stays relevant&lt;/li&gt;
&lt;li&gt;your migration history does not become framework glue&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In database tooling, that is a strong design choice.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final thoughts
&lt;/h2&gt;

&lt;p&gt;There are plenty of PostgreSQL migration tools out there. Many are good. But a lot of them drift toward abstraction for its own sake.&lt;/p&gt;

&lt;p&gt;If what you want is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;PostgreSQL migrations&lt;/li&gt;
&lt;li&gt;plain SQL files&lt;/li&gt;
&lt;li&gt;explicit rollbacks&lt;/li&gt;
&lt;li&gt;repeatable migrations&lt;/li&gt;
&lt;li&gt;non-transaction support&lt;/li&gt;
&lt;li&gt;advisory locking&lt;/li&gt;
&lt;li&gt;transactional safety&lt;/li&gt;
&lt;li&gt;hash-based change detection&lt;/li&gt;
&lt;li&gt;flexible directory layouts&lt;/li&gt;
&lt;li&gt;clean CLI usage&lt;/li&gt;
&lt;li&gt;minimal ceremony&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;then &lt;strong&gt;gopgmigrate&lt;/strong&gt; is worth a look.&lt;/p&gt;

&lt;p&gt;It takes a very practical path: keep migrations human-readable, keep behavior explicit, and keep the tool small enough that you can trust what it is doing.&lt;/p&gt;

&lt;p&gt;That is a solid direction for database change management.&lt;/p&gt;

&lt;p&gt;If you find gopgmigrate useful, consider giving the repo a star on GitHub. It helps more people discover the project.&lt;/p&gt;

&lt;p&gt;Repository: &lt;a href="https://github.com/hashmap-kz/gopgmigrate" rel="noopener noreferrer"&gt;https://github.com/hashmap-kz/gopgmigrate&lt;/a&gt;&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>go</category>
      <category>database</category>
    </item>
    <item>
      <title>Finding Hidden Bottlenecks in Go Apps: A Lazy, Hacky, and Bruteforce Method</title>
      <dc:creator>alexey.zh</dc:creator>
      <pubDate>Thu, 02 Apr 2026 07:23:37 +0000</pubDate>
      <link>https://dev.to/alzhi_f93e67fa45b972/finding-hidden-bottlenecks-in-go-apps-a-lazy-hacky-and-bruteforce-method-3dhb</link>
      <guid>https://dev.to/alzhi_f93e67fa45b972/finding-hidden-bottlenecks-in-go-apps-a-lazy-hacky-and-bruteforce-method-3dhb</guid>
      <description>&lt;p&gt;When developing &lt;a href="https://github.com/pgrwl/pgrwl" rel="noopener noreferrer"&gt;pgrwl&lt;/a&gt; - a PostgreSQL WAL receiver - performance is a critical concern.&lt;/p&gt;

&lt;p&gt;Every part of the program must be predictable. There should be no hidden bottlenecks.&lt;/p&gt;

&lt;p&gt;But what about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;typos that silently degrade performance?&lt;/li&gt;
&lt;li&gt;missing tests that fail to catch inefficiencies?&lt;/li&gt;
&lt;li&gt;slow logic introduced "just for now"?&lt;/li&gt;
&lt;li&gt;accidental O(n^2) behavior?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These issues are often hard to detect.&lt;/p&gt;




&lt;h3&gt;
  
  
  The Problem
&lt;/h3&gt;

&lt;p&gt;For instance, you may concatenate a huge template in a loop, but that may be done once outside of the loop. And this will work fine, until the heavy load is reveal that.&lt;/p&gt;

&lt;p&gt;Of course you should profile CPU/RAM, and there are a lot of great tools, but sometimes it's not enough.&lt;/p&gt;




&lt;h3&gt;
  
  
  The Bruteforce Idea
&lt;/h3&gt;

&lt;p&gt;A lazy decision for measuring the whole picture is to to trace each function execution time, and the total number that function being called. &lt;/p&gt;

&lt;p&gt;Yeah, that cannot help you to inspect all the loops, nasty conditions, memory leaks, etc... But may help a LOT to find as fast as possible really heavily loaded functions, and start profiling them deeply with more advanced profiling tools.&lt;/p&gt;




&lt;h3&gt;
  
  
  The Solution
&lt;/h3&gt;

&lt;p&gt;I wrote a KISS &lt;a href="https://github.com/hashmap-kz/gotrackfunc" rel="noopener noreferrer"&gt;library&lt;/a&gt; called &lt;code&gt;gotrackfunc&lt;/code&gt; that injects timing into each single function in the whole project at one CLI command.&lt;/p&gt;




&lt;h3&gt;
  
  
  How it works
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;gotrackfunc&lt;/code&gt; injects timing code into all functions&lt;/li&gt;
&lt;li&gt;Run your program&lt;/li&gt;
&lt;li&gt;Apply load&lt;/li&gt;
&lt;li&gt;Stop execution&lt;/li&gt;
&lt;li&gt;Analyze the report&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;It's rough and primitive, but it works!!!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You MUST have a version control system of course, so you can drop&lt;br&gt;
all these changes.&lt;/strong&gt;&lt;/p&gt;


&lt;h3&gt;
  
  
  Usage And Example Output
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# execute in directory of your project&lt;/span&gt;
gotrackfunc ./...

&lt;span class="c"&gt;# run your app (a gotrackfunc.log will produced)&lt;/span&gt;
go run main.go

&lt;span class="c"&gt;# make a report (turn gotrackfunc.log into readable form)&lt;/span&gt;
gotrackfunc summarize
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;In this example I've found that my Put() function is the slowest &lt;br&gt;
part of the whole things. So I can inspect it, refactor, optimize,&lt;br&gt;
write more unit-tests, write integration-tests and measure again.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;FUNCTION                            CALLS  TOTAL_NS     TOTAL_SEC
--------                            -----  --------     ---------
storecrypt.Put                      70     23061361400  23.06
receivesuperv.uploadOneFile         35     11606918000  11.61
fsync.Fsync                         106    8813968000   8.81
xlog.processOneMsg                  4481   6818721600   6.82
xlog.processXLogDataMsg             4481   6814495400   6.81
xlog.CloseWalFile                   35     6561511500   6.56
xlog.closeAndRename                 35     6559979000   6.56
fsync.FsyncFname                    70     6525596900   6.53
receivesuperv.performUploads        2      3036884600   3.04
receivesuperv.uploadFiles           1      3034237400   3.03
fsync.FsyncFnameAndDir              35     435023800    0.44
xlog.WriteAtWalFile                 4481   208340500    0.21
cmd.mustInitPgrw                    1      201671300    0.20
xlog.NewPgReceiver                  1      201671300    0.20
xlog.SyncWalFile                    1      48771200     0.05
codec.Flush                         35     42261100     0.04
xlog.OpenWalFile                    36     16938600     0.02
xlog.createFileAndTruncate          36     11396900     0.01
storecrypt.ListInfo                 4      9813800      0.01
receivesuperv.performRetention      2      4906900      0.00
conv.ToUint64                       4482   2664000      0.00
receivesuperv.filterFilesToUpload   2      2647200      0.00
fsx.FileExists                      35     2628000      0.00
xlog.XLogSegmentOffset              8963   2083300      0.00
conv.Uint64ToInt64                  4517   1656300      0.00
cmd.loadConfig                      1      1563600      0.00
config.MustLoad                     1      1563600      0.00
config.mustLoadCfg                  1      1563600      0.00
pipe.CompressAndEncryptOptional     35     1278200      0.00
receivemetrics.AddWALBytesReceived  4481   1121600      0.00
codec.Close                         35     1001200      0.00
xlog.sendFeedback                   3      690700       0.00
xlog.findStreamingStart             1      608600       0.00
receivemode.Init                    1      608600       0.00
storecrypt.NewLocal                 1      522500       0.00
xlog.GetSlotInformation             2      522500       0.00
shared.SetupStorage                 1      522500       0.00
xlog.parseReadReplicationSlot       2      522500       0.00
cmd.mustInitStorageIfRequired       1      522500       0.00
codec.NewWriter                     35     504900       0.00
storecrypt.fullPath                 37     504000       0.00
xlog.XLogFileName                   36     42900        0.00
shared.InitOptionalHandlers         1      0            0.00
config.IsLocalStor                  3      0            0.00
jobq.Start                          1      0            0.00
receivemetrics.IncJobsExecuted      4      0            0.00
receivemetrics.IncWALFilesReceived  35     0            0.00
xlog.IsPartialXLogFileName          2      0            0.00
receivemetrics.ObserveJobDuration   4      0            0.00
xlog.ScanWalSegSize                 1      0            0.00
cmd.needSupervisorLoop              1      0            0.00
shared.getWriteExt                  1      0            0.00
storecrypt.transformsFromName       35     0            0.00
config.checkBackupConfig            1      0            0.00
receivemode.NewReceiveModeService   1      0            0.00
conv.ParseUint32                    2      0            0.00
storecrypt.isSupportedWriteExt      1      0            0.00
receivemode.NewReceiveController    1      0            0.00
receivemetrics.IncJobsSubmitted     4      0            0.00
config.checkMode                    1      0            0.00
storecrypt.encodePath               35     0            0.00
config.expandEnvsWithPrefix         1      0            0.00
config.checkLogConfig               1      0            0.00
xlog.IsPowerOf2                     1      0            0.00
aesgcm.NewChunkedGCMCrypter         1      0            0.00
shared.NewHTTPSrv                   1      0            0.00
xlog.XLogSegmentsPerXLogId          72     0            0.00
xlog.IsValidWalSegSize              1      0            0.00
config.checkMainConfig              1      0            0.00
config.checkStorageConfig           1      0            0.00
logger.Init                         1      0            0.00
receivesuperv.NewArchiveSupervisor  1      0            0.00
middleware.Middleware               6      0            0.00
xlog.existsTimeLineHistoryFile      1      0            0.00
xlog.IsXLogFileName                 2      0            0.00
config.IsExternalStor               1      0            0.00
receivesuperv.log                   83     0            0.00
cmd.App                             1      0            0.00
xlog.parseShowParameter             2      0            0.00
config.expand                       1      0            0.00
jobq.log                            8      0            0.00
xlog.updateLastFlushPosition        37     0            0.00
receivemetrics.IncWALFilesUploaded  35     0            0.00
strx.HeredocTrim                    1      0            0.00
cmd.checkPgEnvsAreSet               1      0            0.00
storecrypt.NewVariadicStorage       1      0            0.00
middleware.Chain                    1      0            0.00
xlog.SetStream                      1      0            0.00
jobq.Submit                         4      0            0.00
config.String                       1      0            0.00
config.validate                     1      0            0.00
jobq.NewJobQueue                    1      0            0.00
config.checkReceiverConfig          1      0            0.00
xlog.calculateCopyStreamSleepTime   3      0            0.00
middleware.SafeHandlerMiddleware    3      0            0.00
xlog.NewStream                      1      0            0.00
conv.Uint32ToInt32                  1      0            0.00
xlog.XLByteToSeg                    36     0            0.00
cmd.initMetrics                     1      0            0.00
xlog.GetShowParameter               2      0            0.00
shared.log                          1      0            0.00
xlog.log                            107    0            0.00
config.Cfg                          3      0            0.00
receivesuperv.filterOlderThan       2      0            0.00
storecrypt.decodePath               70     0            0.00
storecrypt.supportedExts            70     0            0.00
xlog.CurrentOpenWALFileName         37     0            0.00
config.checkStorageModifiersConfig  1      0            0.00
xlog.GetStartupInfo                 1      0            0.00
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






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

&lt;p&gt;This approach is rough, primitive - and surprisingly effective.&lt;/p&gt;

&lt;p&gt;Sometimes, brute force wins.&lt;/p&gt;




&lt;h3&gt;
  
  
  Links
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;WAL receiver project: &lt;a href="https://github.com/pgrwl/pgrwl" rel="noopener noreferrer"&gt;https://github.com/pgrwl/pgrwl&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Tracking library: &lt;a href="https://github.com/hashmap-kz/gotrackfunc" rel="noopener noreferrer"&gt;https://github.com/hashmap-kz/gotrackfunc&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>go</category>
      <category>postgres</category>
    </item>
    <item>
      <title>Backup Is Not Enough: A PostgreSQL Recovery Story</title>
      <dc:creator>alexey.zh</dc:creator>
      <pubDate>Tue, 31 Mar 2026 13:57:06 +0000</pubDate>
      <link>https://dev.to/alzhi_f93e67fa45b972/backup-is-not-enough-a-postgresql-recovery-story-26cd</link>
      <guid>https://dev.to/alzhi_f93e67fa45b972/backup-is-not-enough-a-postgresql-recovery-story-26cd</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;This experiment is designed to &lt;strong&gt;test and validate the pgrwl tool in&lt;br&gt;
real conditions&lt;/strong&gt;: &lt;a href="https://github.com/pgrwl/pgrwl" rel="noopener noreferrer"&gt;https://github.com/pgrwl/pgrwl&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Instead of synthetic examples, we simulate a real-world failure and&lt;br&gt;
verify that recovery actually works end-to-end.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Let’s do something slightly uncomfortable.&lt;/p&gt;

&lt;p&gt;We're going to simulate a database crash and recovery.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Think disk failure. Whole server gone.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;And then bring it back - byte for byte - as if nothing happened.&lt;/p&gt;

&lt;p&gt;Not "some" data.&lt;br&gt;
Not "close enough".&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Everything.&lt;/strong&gt;&lt;/p&gt;


&lt;h1&gt;
  
  
  The Myth of "Backups"
&lt;/h1&gt;

&lt;p&gt;Most people think:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"I have a backup, so I'm safe."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's... half true.&lt;/p&gt;

&lt;p&gt;A &lt;strong&gt;base backup&lt;/strong&gt; is just a snapshot - a frozen picture of your&lt;br&gt;
database at one moment.&lt;/p&gt;

&lt;p&gt;But databases don't sit still.&lt;/p&gt;

&lt;p&gt;Every insert, update, delete - all of that happens &lt;strong&gt;after&lt;/strong&gt; your&lt;br&gt;
backup.&lt;/p&gt;

&lt;p&gt;So where does that data live?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;-&amp;gt; In WAL (Write-Ahead Log)&lt;/strong&gt;&lt;/p&gt;


&lt;h1&gt;
  
  
  The Real Rule
&lt;/h1&gt;

&lt;p&gt;If you remember one thing from this post, let it be this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Recovery = Base Backup + WAL&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Without WAL:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;your backup is outdated&lt;/li&gt;
&lt;li&gt;your data is incomplete&lt;/li&gt;
&lt;li&gt;your recovery is a lie&lt;/li&gt;
&lt;/ul&gt;


&lt;h1&gt;
  
  
  The Experiment
&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;Warning: this is not intended to be run on production environment&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Note: For simplicity, both the database and the backup tool are running on the same machine. In production, you should never store backups on the same host where the database is running.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;We'll prove this using a bunch of simple shell commands.&lt;/p&gt;

&lt;p&gt;Note: env-vars are omitted for simplicity.&lt;/p&gt;

&lt;p&gt;A full working script will be attached at the end of the article.&lt;/p&gt;


&lt;h1&gt;
  
  
  Step 1 --- Build a Database From Nothing
&lt;/h1&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;log &lt;span class="s2"&gt;"Initializing PostgreSQL cluster..."&lt;/span&gt;
initdb &lt;span class="nt"&gt;-D&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PGDATA&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-A&lt;/span&gt; trust &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--auth-local&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;trust &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--auth-host&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;trust &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null

log &lt;span class="s2"&gt;"Starting PostgreSQL..."&lt;/span&gt;
pg_ctl &lt;span class="nt"&gt;-D&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PGDATA&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$WORKDIR&lt;/span&gt;&lt;span class="s2"&gt;/pg.log"&lt;/span&gt; start &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null
wait_for_postgres

log &lt;span class="s2"&gt;"Creating physical replication slot: &lt;/span&gt;&lt;span class="nv"&gt;$REPL_SLOT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
psql &lt;span class="nt"&gt;-d&lt;/span&gt; postgres &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="nv"&gt;ON_ERROR_STOP&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"select pg_create_physical_replication_slot('&lt;/span&gt;&lt;span class="nv"&gt;$REPL_SLOT&lt;/span&gt;&lt;span class="s2"&gt;');"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null

log &lt;span class="s2"&gt;"Creating test database: &lt;/span&gt;&lt;span class="nv"&gt;$DBNAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
createdb &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DBNAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;We start from zero.&lt;/p&gt;


&lt;h1&gt;
  
  
  Step 2 --- Start Capturing WAL
&lt;/h1&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;log &lt;span class="s2"&gt;"Writing pgrwl configuration..."&lt;/span&gt;
&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PGRWL_CONFIG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;
{
  "main": {
    "listen_port": 7070,
    "directory": "&lt;/span&gt;&lt;span class="nv"&gt;$WAL_ARCHIVE_DIR&lt;/span&gt;&lt;span class="sh"&gt;"
  },
  "receiver": {
    "slot": "&lt;/span&gt;&lt;span class="nv"&gt;$REPL_SLOT&lt;/span&gt;&lt;span class="sh"&gt;",
    "no_loop": true
  },
  "log": {
    "level": "debug",
    "format": "text",
    "add_source": false
  }
}
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;log &lt;span class="s2"&gt;"Starting pgrwl receiver..."&lt;/span&gt;
pgrwl daemon &lt;span class="nt"&gt;-m&lt;/span&gt; receive &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PGRWL_CONFIG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$WORKDIR&lt;/span&gt;&lt;span class="s2"&gt;/pgrwl-receive.log"&lt;/span&gt; 2&amp;gt;&amp;amp;1 &amp;amp;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Starting &lt;a href="https://github.com/pgrwl/pgrwl" rel="noopener noreferrer"&gt;pgrwl&lt;/a&gt; in a &lt;code&gt;receive&lt;/code&gt; mode&lt;/p&gt;


&lt;h1&gt;
  
  
  Step 3 --- Take a Base Backup
&lt;/h1&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;log &lt;span class="s2"&gt;"Creating base backup..."&lt;/span&gt;
pgrwl backup &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PGRWL_CONFIG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This is your snapshot in time.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Backup created by using PostgreSQL replication protocol (i.e. additional tools are not required).&lt;/em&gt;&lt;/p&gt;


&lt;h1&gt;
  
  
  Step 4 --- Populate DB
&lt;/h1&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;log &lt;span class="s2"&gt;"Initializing pgbench data (scale=10 ~ about 1 million rows in pgbench_accounts)..."&lt;/span&gt;
pgbench &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; 10 &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DBNAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

log &lt;span class="s2"&gt;"Running pgbench workload..."&lt;/span&gt;
pgbench &lt;span class="nt"&gt;-c&lt;/span&gt; 4 &lt;span class="nt"&gt;-j&lt;/span&gt; 2 &lt;span class="nt"&gt;-t&lt;/span&gt; 200 &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DBNAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;All this data exists ONLY in WAL.&lt;/p&gt;


&lt;h1&gt;
  
  
  Step 5 --- Save the Truth
&lt;/h1&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;log &lt;span class="s2"&gt;"Dumping cluster state before destruction..."&lt;/span&gt;
pg_dumpall &lt;span class="nt"&gt;--quote-all-identifiers&lt;/span&gt; &lt;span class="nt"&gt;--restrict-key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0 &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$WORKDIR&lt;/span&gt;&lt;span class="s2"&gt;/before.sql"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This dump becomes our ground truth.&lt;br&gt;
After restore + WAL replay, we expect the cluster to match this state.&lt;/p&gt;


&lt;h1&gt;
  
  
  Step 6 --- Delete Everything
&lt;/h1&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;log &lt;span class="s2"&gt;"Stopping PostgreSQL and pgrwl receiver..."&lt;/span&gt;
stop_postgres
stop_pgrwl_receive

log &lt;span class="s2"&gt;"Removing original PGDATA to simulate data loss..."&lt;/span&gt;
&lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PGDATA&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Database gone.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No tables.&lt;/li&gt;
&lt;li&gt;No data.&lt;/li&gt;
&lt;li&gt;No second chances.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Only backup and WAL remain.&lt;/p&gt;


&lt;h1&gt;
  
  
  Step 7 --- Restore the Base Backup
&lt;/h1&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;log &lt;span class="s2"&gt;"Restoring PGDATA from base backup..."&lt;/span&gt;
pgrwl restore &lt;span class="nt"&gt;--dest&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PGDATA&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PGRWL_CONFIG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="nb"&gt;chmod &lt;/span&gt;0750 &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PGDATA&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;chown&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; postgres:postgres &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PGDATA&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="c"&gt;# recovery.signal tells PostgreSQL to start in archive recovery mode.&lt;/span&gt;
&lt;span class="nb"&gt;touch&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PGDATA&lt;/span&gt;&lt;span class="s2"&gt;/recovery.signal"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;We are back to snapshot state only.&lt;/p&gt;


&lt;h1&gt;
  
  
  Step 8 --- Replay History
&lt;/h1&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;log &lt;span class="s2"&gt;"Starting pgrwl restore server..."&lt;/span&gt;
pgrwl daemon &lt;span class="nt"&gt;-m&lt;/span&gt; serve &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PGRWL_CONFIG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$WORKDIR&lt;/span&gt;&lt;span class="s2"&gt;/pgrwl-serve.log"&lt;/span&gt; 2&amp;gt;&amp;amp;1 &amp;amp;
&lt;span class="nv"&gt;PGRWL_SERVE_PID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$!&lt;/span&gt;

&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PGDATA&lt;/span&gt;&lt;span class="s2"&gt;/postgresql.conf"&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;
restore_command = 'pgrwl restore-command --serve-addr=127.0.0.1:7070 %f %p'
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;log &lt;span class="s2"&gt;"Starting restored PostgreSQL cluster..."&lt;/span&gt;
pg_ctl &lt;span class="nt"&gt;-D&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PGDATA&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$WORKDIR&lt;/span&gt;&lt;span class="s2"&gt;/postgres-restored.log"&lt;/span&gt; start &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null

wait_for_postgres
wait_until_out_of_recovery
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Start pgrwl in serve mode for restore_command, run cluster, PostgreSQL starts replaying WAL.&lt;/p&gt;

&lt;p&gt;It is &lt;strong&gt;replaying history&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Every insert.&lt;br&gt;
Every update.&lt;br&gt;
Every commit.&lt;/p&gt;

&lt;p&gt;Reconstructed from WAL.&lt;/p&gt;


&lt;h1&gt;
  
  
  Step 9 --- Did It Work?
&lt;/h1&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;log &lt;span class="s2"&gt;"Dumping cluster state after recovery..."&lt;/span&gt;
pg_dumpall &lt;span class="nt"&gt;--quote-all-identifiers&lt;/span&gt; &lt;span class="nt"&gt;--restrict-key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0 &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$WORKDIR&lt;/span&gt;&lt;span class="s2"&gt;/after.sql"&lt;/span&gt;

log &lt;span class="s2"&gt;"Comparing dumps..."&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;diff &lt;span class="nt"&gt;-u&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$WORKDIR&lt;/span&gt;&lt;span class="s2"&gt;/before.sql"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$WORKDIR&lt;/span&gt;&lt;span class="s2"&gt;/after.sql"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$WORKDIR&lt;/span&gt;&lt;span class="s2"&gt;/dump.diff"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;log &lt;span class="s2"&gt;"SUCCESS: restored cluster matches original state"&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"before: &lt;/span&gt;&lt;span class="nv"&gt;$WORKDIR&lt;/span&gt;&lt;span class="s2"&gt;/before.sql"&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"after : &lt;/span&gt;&lt;span class="nv"&gt;$WORKDIR&lt;/span&gt;&lt;span class="s2"&gt;/after.sql"&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"diff  : &lt;/span&gt;&lt;span class="nv"&gt;$WORKDIR&lt;/span&gt;&lt;span class="s2"&gt;/dump.diff (empty)"&lt;/span&gt;
&lt;span class="k"&gt;else
  &lt;/span&gt;&lt;span class="nb"&gt;echo
  echo&lt;/span&gt; &lt;span class="s2"&gt;"FAIL: restored cluster differs from original state"&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"See diff: &lt;/span&gt;&lt;span class="nv"&gt;$WORKDIR&lt;/span&gt;&lt;span class="s2"&gt;/dump.diff"&lt;/span&gt;
  &lt;span class="nb"&gt;exit &lt;/span&gt;1
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;If there is no diff:&lt;/p&gt;

&lt;p&gt;We recovered &lt;strong&gt;every single transaction&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Not approximately. Not logically. &lt;strong&gt;Exactly&lt;/strong&gt;.&lt;/p&gt;


&lt;h1&gt;
  
  
  Mental Model
&lt;/h1&gt;

&lt;p&gt;Think Git:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;backup = commit&lt;/li&gt;
&lt;li&gt;WAL = commits after&lt;/li&gt;
&lt;li&gt;recovery = replay commits&lt;/li&gt;
&lt;/ul&gt;


&lt;h1&gt;
  
  
  Final Thought
&lt;/h1&gt;

&lt;p&gt;If you don't understand WAL, you don't understand PostgreSQL recovery.&lt;/p&gt;


&lt;h1&gt;
  
  
  Using docker environment for integration tests
&lt;/h1&gt;

&lt;p&gt;&lt;a href="https://github.com/pgrwl/pgrwl/tree/master/test/integration/environ" rel="noopener noreferrer"&gt;Integration Tests&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-Eeuo&lt;/span&gt; pipefail

&lt;span class="c"&gt;# setup docker-compose env&lt;/span&gt;
&lt;span class="nb"&gt;cd&lt;/span&gt; /tmp
git clone https://github.com/pgrwl/pgrwl.git
&lt;span class="nb"&gt;cd &lt;/span&gt;pgrwl/test/integration/environ
make restart

&lt;span class="c"&gt;# exec into container&lt;/span&gt;
docker &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-it&lt;/span&gt; pg-primary bash

&lt;span class="c"&gt;# run tests&lt;/span&gt;
su - postgres
&lt;span class="nb"&gt;cd &lt;/span&gt;scripts/tests
bash 011-basic-flow.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h1&gt;
  
  
  Full Script
&lt;/h1&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-Eeuo&lt;/span&gt; pipefail

&lt;span class="c"&gt;###############################################################################&lt;/span&gt;
&lt;span class="c"&gt;# Simple 'Point In Time Recovery' tutorial with pgrwl&lt;/span&gt;
&lt;span class="c"&gt;#&lt;/span&gt;
&lt;span class="c"&gt;# What this script demonstrates:&lt;/span&gt;
&lt;span class="c"&gt;#&lt;/span&gt;
&lt;span class="c"&gt;#   1. Start a fresh PostgreSQL cluster&lt;/span&gt;
&lt;span class="c"&gt;#   2. Start pgrwl in WAL receiver mode&lt;/span&gt;
&lt;span class="c"&gt;#   3. Take a base backup&lt;/span&gt;
&lt;span class="c"&gt;#   4. Generate more data AFTER the base backup&lt;/span&gt;
&lt;span class="c"&gt;#   5. Save a logical dump of the final database state&lt;/span&gt;
&lt;span class="c"&gt;#   6. Destroy PGDATA (simulate disaster)&lt;/span&gt;
&lt;span class="c"&gt;#   7. Restore from the base backup&lt;/span&gt;
&lt;span class="c"&gt;#   8. Replay archived WAL files&lt;/span&gt;
&lt;span class="c"&gt;#   9. Compare the restored database with the original state&lt;/span&gt;
&lt;span class="c"&gt;#&lt;/span&gt;
&lt;span class="c"&gt;# Main idea:&lt;/span&gt;
&lt;span class="c"&gt;#&lt;/span&gt;
&lt;span class="c"&gt;#   A base backup is only a snapshot at one point in time.&lt;/span&gt;
&lt;span class="c"&gt;#   All changes made after that snapshot live in WAL.&lt;/span&gt;
&lt;span class="c"&gt;#   To recover to the latest committed transaction, we need BOTH:&lt;/span&gt;
&lt;span class="c"&gt;#&lt;/span&gt;
&lt;span class="c"&gt;#     - the base backup&lt;/span&gt;
&lt;span class="c"&gt;#     - the WAL generated after the backup&lt;/span&gt;
&lt;span class="c"&gt;#&lt;/span&gt;
&lt;span class="c"&gt;###############################################################################&lt;/span&gt;

&lt;span class="c"&gt;###############################################################################&lt;/span&gt;
&lt;span class="c"&gt;# Configuration&lt;/span&gt;
&lt;span class="c"&gt;###############################################################################&lt;/span&gt;

&lt;span class="nv"&gt;PGDATA&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"/tmp/pgrwl-basic/pgdata"&lt;/span&gt;
&lt;span class="nv"&gt;WAL_ARCHIVE_DIR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"/tmp/pgrwl-basic/wal-archive"&lt;/span&gt;
&lt;span class="nv"&gt;PGRWL_CONFIG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"/tmp/pgrwl-basic/pgrwl-config.json"&lt;/span&gt;

&lt;span class="nv"&gt;DBNAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"bench"&lt;/span&gt;
&lt;span class="nv"&gt;REPL_SLOT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"pgrwl_v5"&lt;/span&gt;

&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;PGHOST&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"localhost"&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;PGPORT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"5432"&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;PGUSER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"postgres"&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;PGPASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"postgres"&lt;/span&gt;

&lt;span class="nv"&gt;PGRWL_RECEIVE_PID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;
&lt;span class="nv"&gt;PGRWL_SERVE_PID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;

&lt;span class="c"&gt;###############################################################################&lt;/span&gt;
&lt;span class="c"&gt;# Small helper functions&lt;/span&gt;
&lt;span class="c"&gt;###############################################################################&lt;/span&gt;

log&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'\n[%s] %s\n'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="s1"&gt;'+%F %T'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$*&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

die&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"ERROR: &lt;/span&gt;&lt;span class="nv"&gt;$*&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp;2
  &lt;span class="nb"&gt;exit &lt;/span&gt;1
&lt;span class="o"&gt;}&lt;/span&gt;

wait_for_postgres&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  log &lt;span class="s2"&gt;"Waiting for PostgreSQL to accept connections..."&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;_ &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;seq &lt;/span&gt;1 120&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
    if &lt;/span&gt;pg_isready &lt;span class="nt"&gt;-h&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PGHOST&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PGPORT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-U&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PGUSER&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null 2&amp;gt;&amp;amp;1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
      return &lt;/span&gt;0
    &lt;span class="k"&gt;fi
    &lt;/span&gt;&lt;span class="nb"&gt;sleep &lt;/span&gt;1
  &lt;span class="k"&gt;done
  &lt;/span&gt;die &lt;span class="s2"&gt;"PostgreSQL did not become ready in time"&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

wait_until_out_of_recovery&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  log &lt;span class="s2"&gt;"Waiting for PostgreSQL to finish recovery..."&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;_ &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;seq &lt;/span&gt;1 120&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
    if &lt;/span&gt;psql &lt;span class="nt"&gt;-d&lt;/span&gt; postgres &lt;span class="nt"&gt;-Atqc&lt;/span&gt; &lt;span class="s2"&gt;"select pg_is_in_recovery()"&lt;/span&gt; 2&amp;gt;/dev/null | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="s1"&gt;'^f$'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
      return &lt;/span&gt;0
    &lt;span class="k"&gt;fi
    &lt;/span&gt;&lt;span class="nb"&gt;sleep &lt;/span&gt;1
  &lt;span class="k"&gt;done
  &lt;/span&gt;die &lt;span class="s2"&gt;"PostgreSQL did not finish recovery in time"&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

stop_postgres&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PGDATA&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;log &lt;span class="s2"&gt;"Stopping PostgreSQL..."&lt;/span&gt;
    pg_ctl &lt;span class="nt"&gt;-D&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PGDATA&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-m&lt;/span&gt; immediate stop &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null 2&amp;gt;&amp;amp;1 &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true
  &lt;/span&gt;&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

stop_pgrwl_receive&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PGRWL_RECEIVE_PID&lt;/span&gt;&lt;span class="k"&gt;:-}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;log &lt;span class="s2"&gt;"Stopping pgrwl receiver..."&lt;/span&gt;
    &lt;span class="nb"&gt;kill&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PGRWL_RECEIVE_PID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null 2&amp;gt;&amp;amp;1 &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true
    wait&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PGRWL_RECEIVE_PID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null 2&amp;gt;&amp;amp;1 &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true
    &lt;/span&gt;&lt;span class="nv"&gt;PGRWL_RECEIVE_PID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;
  &lt;span class="k"&gt;fi&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

stop_pgrwl_serve&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PGRWL_SERVE_PID&lt;/span&gt;&lt;span class="k"&gt;:-}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;log &lt;span class="s2"&gt;"Stopping pgrwl restore server..."&lt;/span&gt;
    &lt;span class="nb"&gt;kill&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PGRWL_SERVE_PID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null 2&amp;gt;&amp;amp;1 &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true
    wait&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PGRWL_SERVE_PID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null 2&amp;gt;&amp;amp;1 &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true
    &lt;/span&gt;&lt;span class="nv"&gt;PGRWL_SERVE_PID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;
  &lt;span class="k"&gt;fi&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

cleanup&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  stop_pgrwl_receive
  stop_pgrwl_serve
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="nb"&gt;trap &lt;/span&gt;cleanup EXIT

&lt;span class="c"&gt;###############################################################################&lt;/span&gt;
&lt;span class="c"&gt;# Phase 0. Start from a clean state&lt;/span&gt;
&lt;span class="c"&gt;###############################################################################&lt;/span&gt;

log &lt;span class="s2"&gt;"Cleaning up old processes and files..."&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;pkill &lt;span class="nt"&gt;-9&lt;/span&gt; postgres &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true
sudo &lt;/span&gt;pkill &lt;span class="nt"&gt;-9&lt;/span&gt; pgrwl &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true
sudo rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; &lt;span class="s2"&gt;"/tmp/pgrwl-basic"&lt;/span&gt;

log &lt;span class="s2"&gt;"Preparing work directory: /tmp/pgrwl-basic"&lt;/span&gt;
&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"/tmp/pgrwl-basic"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$WAL_ARCHIVE_DIR&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="c"&gt;###############################################################################&lt;/span&gt;
&lt;span class="c"&gt;# Phase 1. Create and start a fresh PostgreSQL cluster&lt;/span&gt;
&lt;span class="c"&gt;###############################################################################&lt;/span&gt;

log &lt;span class="s2"&gt;"Initializing PostgreSQL cluster..."&lt;/span&gt;
initdb &lt;span class="nt"&gt;-D&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PGDATA&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-A&lt;/span&gt; trust &lt;span class="nt"&gt;--auth-local&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;trust &lt;span class="nt"&gt;--auth-host&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;trust &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null

&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PGDATA&lt;/span&gt;&lt;span class="s2"&gt;/postgresql.conf"&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;
listen_addresses      = '*'

# Settings required for WAL streaming / archiving style workflows
wal_level                = replica
max_wal_senders          = 10
max_replication_slots    = 10
wal_keep_size            = 64MB

# Durability settings
fsync                    = on
synchronous_commit       = on
full_page_writes         = on

# Basic logging settings
log_directory            = '/tmp/pgrwl-basic'
log_filename             = 'pg.log'
log_lock_waits           = on
log_temp_files           = 0
log_checkpoints          = on
log_connections          = off
log_destination          = 'stderr'
log_error_verbosity      = 'DEFAULT' # TERSE, DEFAULT, VERBOSE
log_hostname             = off
log_min_messages         = 'WARNING' # DEBUG5, DEBUG4, DEBUG3, DEBUG2, DEBUG1, INFO, NOTICE, WARNING, ERROR, LOG, FATAL, PANIC
log_timezone             = 'Asia/Aqtau'
log_line_prefix          = '%t [%p-%l] %r %q%u@%d '
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;log &lt;span class="s2"&gt;"Starting PostgreSQL..."&lt;/span&gt;
pg_ctl &lt;span class="nt"&gt;-D&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PGDATA&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt; &lt;span class="s2"&gt;"/tmp/pgrwl-basic/pg.log"&lt;/span&gt; start &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null
wait_for_postgres

log &lt;span class="s2"&gt;"Creating physical replication slot: &lt;/span&gt;&lt;span class="nv"&gt;$REPL_SLOT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
psql &lt;span class="nt"&gt;-d&lt;/span&gt; postgres &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="nv"&gt;ON_ERROR_STOP&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"select pg_create_physical_replication_slot('&lt;/span&gt;&lt;span class="nv"&gt;$REPL_SLOT&lt;/span&gt;&lt;span class="s2"&gt;');"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null

log &lt;span class="s2"&gt;"Creating test database: &lt;/span&gt;&lt;span class="nv"&gt;$DBNAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
createdb &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DBNAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="c"&gt;###############################################################################&lt;/span&gt;
&lt;span class="c"&gt;# Phase 2. Configure and start pgrwl in receive mode&lt;/span&gt;
&lt;span class="c"&gt;###############################################################################&lt;/span&gt;

log &lt;span class="s2"&gt;"Writing pgrwl configuration..."&lt;/span&gt;
&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PGRWL_CONFIG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;
{
  "main": {
    "listen_port": 7070,
    "directory": "&lt;/span&gt;&lt;span class="nv"&gt;$WAL_ARCHIVE_DIR&lt;/span&gt;&lt;span class="sh"&gt;"
  },
  "receiver": {
    "slot": "&lt;/span&gt;&lt;span class="nv"&gt;$REPL_SLOT&lt;/span&gt;&lt;span class="sh"&gt;",
    "no_loop": true
  },
  "log": {
    "level": "debug",
    "format": "text",
    "add_source": false
  }
}
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;log &lt;span class="s2"&gt;"Starting pgrwl receiver..."&lt;/span&gt;
pgrwl daemon &lt;span class="nt"&gt;-m&lt;/span&gt; receive &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PGRWL_CONFIG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="s2"&gt;"/tmp/pgrwl-basic/pgrwl-receive.log"&lt;/span&gt; 2&amp;gt;&amp;amp;1 &amp;amp;
&lt;span class="nv"&gt;PGRWL_RECEIVE_PID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$!&lt;/span&gt;

&lt;span class="c"&gt;# Give the receiver a moment to connect and begin streaming.&lt;/span&gt;
&lt;span class="nb"&gt;sleep &lt;/span&gt;3

&lt;span class="c"&gt;###############################################################################&lt;/span&gt;
&lt;span class="c"&gt;# Phase 3. Take a base backup&lt;/span&gt;
&lt;span class="c"&gt;###############################################################################&lt;/span&gt;

log &lt;span class="s2"&gt;"Creating base backup..."&lt;/span&gt;
pgrwl backup &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PGRWL_CONFIG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="c"&gt;###############################################################################&lt;/span&gt;
&lt;span class="c"&gt;# Phase 4. Generate data AFTER the base backup&lt;/span&gt;
&lt;span class="c"&gt;#&lt;/span&gt;
&lt;span class="c"&gt;# This is the important part.&lt;/span&gt;
&lt;span class="c"&gt;# If we recover only from the base backup, these changes would be lost.&lt;/span&gt;
&lt;span class="c"&gt;# They survive only because the WAL receiver captures the WAL stream.&lt;/span&gt;
&lt;span class="c"&gt;###############################################################################&lt;/span&gt;

log &lt;span class="s2"&gt;"Initializing pgbench data (scale=10 ~ about 1 million rows in pgbench_accounts)..."&lt;/span&gt;
pgbench &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; 10 &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DBNAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

log &lt;span class="s2"&gt;"Running pgbench workload..."&lt;/span&gt;
pgbench &lt;span class="nt"&gt;-c&lt;/span&gt; 4 &lt;span class="nt"&gt;-j&lt;/span&gt; 2 &lt;span class="nt"&gt;-t&lt;/span&gt; 200 &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DBNAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="c"&gt;###############################################################################&lt;/span&gt;
&lt;span class="c"&gt;# Phase 5. Save the final logical state before disaster&lt;/span&gt;
&lt;span class="c"&gt;#&lt;/span&gt;
&lt;span class="c"&gt;# This dump becomes our ground truth.&lt;/span&gt;
&lt;span class="c"&gt;# After restore + WAL replay, we expect the cluster to match this state.&lt;/span&gt;
&lt;span class="c"&gt;###############################################################################&lt;/span&gt;

log &lt;span class="s2"&gt;"Dumping cluster state before destruction..."&lt;/span&gt;
pg_dumpall &lt;span class="nt"&gt;--quote-all-identifiers&lt;/span&gt; &lt;span class="nt"&gt;--restrict-key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0 &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="s2"&gt;"/tmp/pgrwl-basic/before.sql"&lt;/span&gt;

&lt;span class="c"&gt;###############################################################################&lt;/span&gt;
&lt;span class="c"&gt;# Phase 6. Force PostgreSQL to emit final WAL and let receiver catch up&lt;/span&gt;
&lt;span class="c"&gt;###############################################################################&lt;/span&gt;

log &lt;span class="s2"&gt;"Forcing checkpoint and WAL switch..."&lt;/span&gt;
psql &lt;span class="nt"&gt;-d&lt;/span&gt; postgres &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="nv"&gt;ON_ERROR_STOP&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1 &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"checkpoint;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null
psql &lt;span class="nt"&gt;-d&lt;/span&gt; postgres &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="nv"&gt;ON_ERROR_STOP&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1 &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"select pg_switch_wal();"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null

&lt;span class="c"&gt;# Give pgrwl time to receive the last WAL segment(s).&lt;/span&gt;
&lt;span class="nb"&gt;sleep &lt;/span&gt;3

&lt;span class="c"&gt;###############################################################################&lt;/span&gt;
&lt;span class="c"&gt;# Phase 7. Simulate disaster&lt;/span&gt;
&lt;span class="c"&gt;###############################################################################&lt;/span&gt;

log &lt;span class="s2"&gt;"Stopping PostgreSQL and pgrwl receiver..."&lt;/span&gt;
stop_postgres
stop_pgrwl_receive

log &lt;span class="s2"&gt;"Removing original PGDATA to simulate data loss..."&lt;/span&gt;
&lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PGDATA&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="c"&gt;###############################################################################&lt;/span&gt;
&lt;span class="c"&gt;# Phase 8. Restore the base backup&lt;/span&gt;
&lt;span class="c"&gt;###############################################################################&lt;/span&gt;

log &lt;span class="s2"&gt;"Restoring PGDATA from base backup..."&lt;/span&gt;
pgrwl restore &lt;span class="nt"&gt;--dest&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PGDATA&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PGRWL_CONFIG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="nb"&gt;chmod &lt;/span&gt;0750 &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PGDATA&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;chown&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; postgres:postgres &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PGDATA&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="c"&gt;# recovery.signal tells PostgreSQL to start in archive recovery mode.&lt;/span&gt;
&lt;span class="nb"&gt;touch&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PGDATA&lt;/span&gt;&lt;span class="s2"&gt;/recovery.signal"&lt;/span&gt;

&lt;span class="c"&gt;###############################################################################&lt;/span&gt;
&lt;span class="c"&gt;# Phase 9. Start pgrwl in serve mode for restore_command&lt;/span&gt;
&lt;span class="c"&gt;###############################################################################&lt;/span&gt;

log &lt;span class="s2"&gt;"Starting pgrwl restore server..."&lt;/span&gt;
pgrwl daemon &lt;span class="nt"&gt;-m&lt;/span&gt; serve &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PGRWL_CONFIG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="s2"&gt;"/tmp/pgrwl-basic/pgrwl-serve.log"&lt;/span&gt; 2&amp;gt;&amp;amp;1 &amp;amp;
&lt;span class="nv"&gt;PGRWL_SERVE_PID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$!&lt;/span&gt;

&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PGDATA&lt;/span&gt;&lt;span class="s2"&gt;/postgresql.conf"&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;
restore_command = 'pgrwl restore-command --serve-addr=127.0.0.1:7070 %f %p'
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;&lt;span class="c"&gt;###############################################################################&lt;/span&gt;
&lt;span class="c"&gt;# Phase 10. Start restored PostgreSQL and let it replay WAL&lt;/span&gt;
&lt;span class="c"&gt;###############################################################################&lt;/span&gt;

log &lt;span class="s2"&gt;"Starting restored PostgreSQL cluster..."&lt;/span&gt;
pg_ctl &lt;span class="nt"&gt;-D&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PGDATA&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt; &lt;span class="s2"&gt;"/tmp/pgrwl-basic/postgres-restored.log"&lt;/span&gt; start &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null

wait_for_postgres
wait_until_out_of_recovery

&lt;span class="c"&gt;###############################################################################&lt;/span&gt;
&lt;span class="c"&gt;# Phase 11. Dump restored state and compare&lt;/span&gt;
&lt;span class="c"&gt;###############################################################################&lt;/span&gt;

log &lt;span class="s2"&gt;"Dumping cluster state after recovery..."&lt;/span&gt;
pg_dumpall &lt;span class="nt"&gt;--quote-all-identifiers&lt;/span&gt; &lt;span class="nt"&gt;--restrict-key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0 &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="s2"&gt;"/tmp/pgrwl-basic/after.sql"&lt;/span&gt;

log &lt;span class="s2"&gt;"Comparing dumps..."&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;diff &lt;span class="nt"&gt;-u&lt;/span&gt; &lt;span class="s2"&gt;"/tmp/pgrwl-basic/before.sql"&lt;/span&gt; &lt;span class="s2"&gt;"/tmp/pgrwl-basic/after.sql"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="s2"&gt;"/tmp/pgrwl-basic/dump.diff"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;log &lt;span class="s2"&gt;"SUCCESS: restored cluster matches original state"&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"before: /tmp/pgrwl-basic/before.sql"&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"after : /tmp/pgrwl-basic/after.sql"&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"diff  : /tmp/pgrwl-basic/dump.diff (empty)"&lt;/span&gt;
&lt;span class="k"&gt;else
  &lt;/span&gt;&lt;span class="nb"&gt;echo
  echo&lt;/span&gt; &lt;span class="s2"&gt;"FAIL: restored cluster differs from original state"&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"See diff: /tmp/pgrwl-basic/dump.diff"&lt;/span&gt;
  &lt;span class="nb"&gt;exit &lt;/span&gt;1
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>postgres</category>
      <category>go</category>
      <category>kubernetes</category>
    </item>
    <item>
      <title>PostgreSQL Streaming WAL Archiver and a backup tool in Go (pgrwl)</title>
      <dc:creator>alexey.zh</dc:creator>
      <pubDate>Sat, 28 Mar 2026 08:13:15 +0000</pubDate>
      <link>https://dev.to/alzhi_f93e67fa45b972/postgresql-straming-wal-archiver-in-go-pgrwl-g91</link>
      <guid>https://dev.to/alzhi_f93e67fa45b972/postgresql-straming-wal-archiver-in-go-pgrwl-g91</guid>
      <description>&lt;p&gt;A &lt;strong&gt;production-grade, cloud-native PostgreSQL WAL archiving system&lt;/strong&gt; designed for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;streaming WAL to S3 with compression, encryption, and retention&lt;/li&gt;
&lt;li&gt;Kubernetes-native PostgreSQL backup workflows&lt;/li&gt;
&lt;li&gt;zero data loss and reliable Point-in-Time Recovery (PITR)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Project
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/pgrwl/pgrwl" rel="noopener noreferrer"&gt;https://github.com/pgrwl/pgrwl&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Features
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;WAL receiver (replication protocol)&lt;/li&gt;
&lt;li&gt;Continuous WAL streaming&lt;/li&gt;
&lt;li&gt;Backup to S3 (MinIO, AWS, etc.)&lt;/li&gt;
&lt;li&gt;Backup to SFTP (backup server)&lt;/li&gt;
&lt;li&gt;WAL compression (gzip, zstd-ready)&lt;/li&gt;
&lt;li&gt;WAL encryption (AES-GCM)&lt;/li&gt;
&lt;li&gt;WAL retention management&lt;/li&gt;
&lt;li&gt;WAL monitoring and observability&lt;/li&gt;
&lt;li&gt;Kubernetes &amp;amp; container ready&lt;/li&gt;
&lt;li&gt;Helm chart support&lt;/li&gt;
&lt;li&gt;YAML / JSON / ENV config&lt;/li&gt;
&lt;li&gt;Lightweight single binary&lt;/li&gt;
&lt;li&gt;Structured logging&lt;/li&gt;
&lt;li&gt;Integration tests (containerized)&lt;/li&gt;
&lt;li&gt;Unit tests&lt;/li&gt;
&lt;li&gt;Backup automation (streaming basebackup)&lt;/li&gt;
&lt;li&gt;Continuous backup for PostgreSQL&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Key Capabilities
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Streaming WAL
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Uses PostgreSQL replication protocol&lt;/li&gt;
&lt;li&gt;Supports synchronous WAL streaming&lt;/li&gt;
&lt;li&gt;Enables zero data loss setups&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Storage Backends
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;S3-compatible storage&lt;/li&gt;
&lt;li&gt;SFTP backup servers&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Compression + Encryption
&lt;/h3&gt;

&lt;p&gt;Pipeline based on filename:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;000000010000000000000001.gz.aes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Flow:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;compress -&amp;gt; encrypt -&amp;gt; upload&lt;/li&gt;
&lt;li&gt;download -&amp;gt; decrypt -&amp;gt; decompress&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Architecture
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PostgreSQL
   | (replication protocol)
WAL Receiver
   |
Local FS (fsync)
   |
Uploader (S3 / SFTP)
   |
Retention manager
   |
HTTP server (restore_command)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Continuous Backup
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;real-time WAL streaming&lt;/li&gt;
&lt;li&gt;safe off-site storage&lt;/li&gt;
&lt;li&gt;full PITR support&lt;/li&gt;
&lt;li&gt;near-zero RPO&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Kubernetes Ready
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;run as StatefulSet&lt;/li&gt;
&lt;li&gt;works with StatefulSets / CNPG / Virtual Machines&lt;/li&gt;
&lt;li&gt;deploy via Helm&lt;/li&gt;
&lt;li&gt;GitOps-friendly&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Configuration Example
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;main&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;listen_port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;7070&lt;/span&gt;
  &lt;span class="na"&gt;directory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;wals&lt;/span&gt;
&lt;span class="na"&gt;receiver&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;slot&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pgrwl_v5&lt;/span&gt;
&lt;span class="na"&gt;log&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;level&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;trace&lt;/span&gt;
  &lt;span class="na"&gt;format&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;text&lt;/span&gt;
  &lt;span class="na"&gt;add_source&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Testing
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;integration tests with real PostgreSQL containers&lt;/li&gt;
&lt;li&gt;end-to-end WAL validation&lt;/li&gt;
&lt;li&gt;unit-tested components&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Why pgrwl?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;simple deployment (single binary)&lt;/li&gt;
&lt;li&gt;production-grade reliability&lt;/li&gt;
&lt;li&gt;cloud-native design&lt;/li&gt;
&lt;li&gt;built for Kubernetes and containers&lt;/li&gt;
&lt;li&gt;secure and efficient WAL handling&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Contribute
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Star the repo (&lt;a href="https://github.com/pgrwl/pgrwl" rel="noopener noreferrer"&gt;https://github.com/pgrwl/pgrwl&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;Open issues (&lt;a href="https://github.com/pgrwl/pgrwl/issues" rel="noopener noreferrer"&gt;https://github.com/pgrwl/pgrwl/issues&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;Suggest improvements&lt;/li&gt;
&lt;li&gt;Submit PRs (&lt;a href="https://github.com/pgrwl/pgrwl/blob/master/CONTRIBUTING.md" rel="noopener noreferrer"&gt;https://github.com/pgrwl/pgrwl/blob/master/CONTRIBUTING.md&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;




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

&lt;p&gt;pgrwl is a &lt;strong&gt;lightweight, powerful, production-ready WAL archiving solution&lt;/strong&gt; that brings:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;streaming&lt;/li&gt;
&lt;li&gt;security&lt;/li&gt;
&lt;li&gt;automation&lt;/li&gt;
&lt;li&gt;observability&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;to PostgreSQL backups.&lt;/p&gt;

</description>
      <category>postgressql</category>
      <category>kubernetes</category>
      <category>go</category>
    </item>
    <item>
      <title>Patch-based, environment-aware Kubernetes deployments using plain YAML and zero templating</title>
      <dc:creator>alexey.zh</dc:creator>
      <pubDate>Wed, 25 Jun 2025 14:18:53 +0000</pubDate>
      <link>https://dev.to/alzhi_f93e67fa45b972/patch-based-environment-aware-kubernetes-deployments-using-plain-yaml-and-zero-templating-5gh1</link>
      <guid>https://dev.to/alzhi_f93e67fa45b972/patch-based-environment-aware-kubernetes-deployments-using-plain-yaml-and-zero-templating-5gh1</guid>
      <description>&lt;p&gt;Meet &lt;a href="https://github.com/kubepatch/kubepatch" rel="noopener noreferrer"&gt;kubepatch&lt;/a&gt; — a simple tool for deploying Kubernetes manifests using a patch-based approach.&lt;/p&gt;

&lt;p&gt;Unlike tools that embed logic into YAML or require custom template languages, &lt;code&gt;kubepatch&lt;/code&gt; keeps your &lt;strong&gt;base manifests clean and idiomatic&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Simple&lt;/strong&gt;: No templates, DSLs, or logic in YAML, zero magic&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Predictable&lt;/strong&gt;: No string substitutions or regex hacks&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Safe&lt;/strong&gt;: Only native Kubernetes YAML manifests - readable, valid, untouched&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Layered&lt;/strong&gt;: Patch logic is externalized and explicit via JSON Patch (RFC 6902)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Declarative&lt;/strong&gt;: Cross-environment deployment with predictable, understandable changes&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🛠 Example
&lt;/h2&gt;

&lt;p&gt;Given a base set of manifests for deploy a basic microservice &lt;br&gt;
&lt;a href="https://github.com/kubepatch/kubepatch/tree/master/examples" rel="noopener noreferrer"&gt;see examples&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Service&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;myapp&lt;/span&gt;
  &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;myapp&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;NodePort&lt;/span&gt;
  &lt;span class="na"&gt;selector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;myapp&lt;/span&gt;
  &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;protocol&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;TCP&lt;/span&gt;
      &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8080&lt;/span&gt;
      &lt;span class="na"&gt;targetPort&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8080&lt;/span&gt;

&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;apps/v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deployment&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;myapp&lt;/span&gt;
  &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;myapp&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;replicas&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;
  &lt;span class="na"&gt;selector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;matchLabels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;myapp&lt;/span&gt;
  &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;myapp&lt;/span&gt;
    &lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;containers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;myapp&lt;/span&gt;
          &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;localhost:5000/restapiapp:latest"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A &lt;code&gt;patches/prod.yaml&lt;/code&gt; might look like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;myapp-prod&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;deployment/myapp&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;op&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;replace&lt;/span&gt;
      &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/spec/replicas&lt;/span&gt;
      &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;op&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;replace&lt;/span&gt;
      &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/spec/template/spec/containers/0/image&lt;/span&gt;
      &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;localhost:5000/restapiapp:1.21"&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;op&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;add&lt;/span&gt;
      &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/spec/template/spec/containers/0/env&lt;/span&gt;
      &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;RESTAPIAPP_VERSION&lt;/span&gt;
          &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;prod&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;LOG_LEVEL&lt;/span&gt;
          &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;info&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;op&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;add&lt;/span&gt;
      &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/spec/template/spec/containers/0/resources&lt;/span&gt;
      &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;limits&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;cpu&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;500m"&lt;/span&gt;
          &lt;span class="na"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;512Mi"&lt;/span&gt;
        &lt;span class="na"&gt;requests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;cpu&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;64m"&lt;/span&gt;
          &lt;span class="na"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;128Mi"&lt;/span&gt;
  &lt;span class="na"&gt;service/myapp&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;op&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;add&lt;/span&gt;
      &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/spec/ports/0/nodePort&lt;/span&gt;
      &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;30266&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A &lt;code&gt;patches/dev.yaml&lt;/code&gt; might look like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;myapp-dev&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;deployment/myapp&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;op&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;replace&lt;/span&gt;
      &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/spec/template/spec/containers/0/image&lt;/span&gt;
      &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;localhost:5000/restapiapp:1.22"&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;op&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;add&lt;/span&gt;
      &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/spec/template/spec/containers/0/env&lt;/span&gt;
      &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;RESTAPIAPP_VERSION&lt;/span&gt;
          &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dev&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;LOG_LEVEL&lt;/span&gt;
          &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;debug&lt;/span&gt;
  &lt;span class="na"&gt;service/myapp&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;op&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;add&lt;/span&gt;
      &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/spec/ports/0/nodePort&lt;/span&gt;
      &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;30265&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Apply the appropriate patch set based on the target environment.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubepatch patch &lt;span class="nt"&gt;-f&lt;/span&gt; base/ &lt;span class="nt"&gt;-p&lt;/span&gt; patches/dev.yaml | kubectl apply &lt;span class="nt"&gt;-f&lt;/span&gt; -
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rendered manifest may look like this (note that all labels are set, as well as all patches are applied)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Service&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;myapp-dev&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;myapp-dev&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;nodePort&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;30265&lt;/span&gt;
      &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8080&lt;/span&gt;
      &lt;span class="na"&gt;protocol&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;TCP&lt;/span&gt;
      &lt;span class="na"&gt;targetPort&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8080&lt;/span&gt;
  &lt;span class="na"&gt;selector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;myapp-dev&lt;/span&gt;
  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;NodePort&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;apps/v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deployment&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;myapp-dev&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;myapp-dev&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;replicas&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
  &lt;span class="na"&gt;selector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;matchLabels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;myapp-dev&lt;/span&gt;
  &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;myapp-dev&lt;/span&gt;
    &lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;containers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;RESTAPIAPP_VERSION&lt;/span&gt;
              &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dev&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;LOG_LEVEL&lt;/span&gt;
              &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;debug&lt;/span&gt;
          &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;localhost:5000/restapiapp:1.22&lt;/span&gt;
          &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;myapp&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Installation
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Manual Installation
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Download the latest binary for your platform from
the &lt;a href="https://github.com/kubepatch/kubepatch/releases" rel="noopener noreferrer"&gt;Releases page&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Place the binary in your system's &lt;code&gt;PATH&lt;/code&gt; (e.g., &lt;code&gt;/usr/local/bin&lt;/code&gt;).&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Installation script
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;(&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-euo&lt;/span&gt; pipefail

&lt;span class="nv"&gt;OS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;uname&lt;/span&gt; | &lt;span class="nb"&gt;tr&lt;/span&gt; &lt;span class="s1"&gt;'[:upper:]'&lt;/span&gt; &lt;span class="s1"&gt;'[:lower:]'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nv"&gt;ARCH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;uname&lt;/span&gt; &lt;span class="nt"&gt;-m&lt;/span&gt; | &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s1"&gt;'s/x86_64/amd64/'&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s1"&gt;'s/\(arm\)\(64\)\?.*/\1\2/'&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s1"&gt;'s/aarch64$/arm64/'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nv"&gt;TAG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; https://api.github.com/repos/kubepatch/kubepatch/releases/latest | jq &lt;span class="nt"&gt;-r&lt;/span&gt; .tag_name&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

curl &lt;span class="nt"&gt;-L&lt;/span&gt; &lt;span class="s2"&gt;"https://github.com/kubepatch/kubepatch/releases/download/&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;TAG&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/kubepatch_&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;TAG&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;_&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;OS&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;_&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;ARCH&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.tar.gz"&lt;/span&gt; |
&lt;span class="nb"&gt;tar&lt;/span&gt; &lt;span class="nt"&gt;-xzf&lt;/span&gt; - &lt;span class="nt"&gt;-C&lt;/span&gt; /usr/local/bin &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nb"&gt;chmod&lt;/span&gt; +x /usr/local/bin/kubepatch
&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Package-Based installation (suitable in CI/CD)
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Debian
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt update &lt;span class="nt"&gt;-y&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; curl
curl &lt;span class="nt"&gt;-LO&lt;/span&gt; https://github.com/kubepatch/kubepatch/releases/latest/download/kubepatch_linux_amd64.deb
&lt;span class="nb"&gt;sudo &lt;/span&gt;dpkg &lt;span class="nt"&gt;-i&lt;/span&gt; kubepatch_linux_amd64.deb
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Alpine Linux
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;apk update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; apk add &lt;span class="nt"&gt;--no-cache&lt;/span&gt; bash curl
curl &lt;span class="nt"&gt;-LO&lt;/span&gt; https://github.com/kubepatch/kubepatch/releases/latest/download/kubepatch_linux_amd64.apk
apk add kubepatch_linux_amd64.apk &lt;span class="nt"&gt;--allow-untrusted&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  ✨ Key Features
&lt;/h2&gt;

&lt;h3&gt;
  
  
  JSON Patch Only
&lt;/h3&gt;

&lt;p&gt;Patches are applied using &lt;a href="https://tools.ietf.org/html/rfc6902" rel="noopener noreferrer"&gt;JSON Patch&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;op&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;replace&lt;/span&gt;
  &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/spec/replicas&lt;/span&gt;
  &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every patch is minimal, explicit, and easy to understand. No string manipulation or text templating involved.&lt;/p&gt;

&lt;h3&gt;
  
  
  Plain Kubernetes YAML Manifests
&lt;/h3&gt;

&lt;p&gt;Your base manifests are 100% pure Kubernetes objects - no logic, no annotations, no overrides, no preprocessing. This&lt;br&gt;
ensures:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Easy editing&lt;/li&gt;
&lt;li&gt;Compatibility with other tools&lt;/li&gt;
&lt;li&gt;Clean Git diffs&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Cross-Environment Deploys
&lt;/h3&gt;

&lt;p&gt;Deploy to &lt;code&gt;dev&lt;/code&gt;, &lt;code&gt;staging&lt;/code&gt;, or &lt;code&gt;prod&lt;/code&gt; just by selecting the right set of patches. All logic lives in patch files, not&lt;br&gt;
your base manifests.&lt;/p&gt;
&lt;h3&gt;
  
  
  Common Labels Support
&lt;/h3&gt;

&lt;p&gt;Inject common labels (like &lt;code&gt;env&lt;/code&gt;, &lt;code&gt;team&lt;/code&gt;, &lt;code&gt;app&lt;/code&gt;), including deep paths like pod templates and selectors.&lt;/p&gt;
&lt;h3&gt;
  
  
  Env Var Substitution (in Patch Values Only)
&lt;/h3&gt;

&lt;p&gt;You can inject secrets and configuration values directly into patch files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;op&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;add&lt;/span&gt;
  &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/spec/template/spec/containers/0/env&lt;/span&gt;
  &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;PGPASSWORD&lt;/span&gt;
      &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${IAM_SERVICE_PGPASS}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Strict env-var substitution (prefix-based) is only allowed inside patches - never in base manifests.&lt;/p&gt;

&lt;h2&gt;
  
  
  Feedback
&lt;/h2&gt;

&lt;p&gt;Have a feature request or issue? Feel free to &lt;a href="https://github.com/kubepatch/kubepatch/issues" rel="noopener noreferrer"&gt;open an issue&lt;/a&gt; or submit a PR!&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>go</category>
      <category>devops</category>
    </item>
    <item>
      <title>Apply Kubernetes Manifests Atomically With Rollback</title>
      <dc:creator>alexey.zh</dc:creator>
      <pubDate>Sat, 21 Jun 2025 06:57:27 +0000</pubDate>
      <link>https://dev.to/alzhi_f93e67fa45b972/kubectl-atomic-apply-apply-kubernetes-manifests-atomically-with-rollback-ipm</link>
      <guid>https://dev.to/alzhi_f93e67fa45b972/kubectl-atomic-apply-apply-kubernetes-manifests-atomically-with-rollback-ipm</guid>
      <description>&lt;p&gt;&lt;code&gt;katomik&lt;/code&gt; - Atomic Apply for Kubernetes Manifests with Rollback Support.&lt;/p&gt;

&lt;p&gt;Applies multiple Kubernetes manifests with &lt;strong&gt;all-or-nothing&lt;/strong&gt; guarantees. Like &lt;code&gt;kubectl apply -f&lt;/code&gt;, but transactional:&lt;br&gt;
if any resource fails to apply or become ready, all previously applied resources are rolled back automatically.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/hashmap-kz/katomik" rel="noopener noreferrer"&gt;GitHub Repo →&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvf3wo90u6wmlp7gtjav7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvf3wo90u6wmlp7gtjav7.png" alt="Image description" width="602" height="624"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Features
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Atomic behavior&lt;/strong&gt;: Applies multiple manifests as a unit. If anything fails, restores the original state.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Server-Side Apply&lt;/strong&gt; (SSA): Uses &lt;code&gt;PATCH&lt;/code&gt; with SSA to minimize conflicts and preserve intent.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Status tracking&lt;/strong&gt;: Waits for all resources to become &lt;code&gt;Current&lt;/code&gt; (Ready/Available) before succeeding.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rollback support&lt;/strong&gt;: Automatically restores previous state if apply or wait fails.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recursive&lt;/strong&gt;: Like &lt;code&gt;kubectl&lt;/code&gt;, supports directories and &lt;code&gt;-R&lt;/code&gt; for recursive traversal.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;STDIN support&lt;/strong&gt;: Use &lt;code&gt;-f -&lt;/code&gt; to read from &lt;code&gt;stdin&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Installation
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Manual Installation
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Download the latest binary for your platform from
the &lt;a href="https://github.com/hashmap-kz/katomik/releases" rel="noopener noreferrer"&gt;Releases page&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Place the binary in your system's &lt;code&gt;PATH&lt;/code&gt; (e.g., &lt;code&gt;/usr/local/bin&lt;/code&gt;).&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Installation script
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;(&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-euo&lt;/span&gt; pipefail

&lt;span class="nv"&gt;OS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;uname&lt;/span&gt; | &lt;span class="nb"&gt;tr&lt;/span&gt; &lt;span class="s1"&gt;'[:upper:]'&lt;/span&gt; &lt;span class="s1"&gt;'[:lower:]'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nv"&gt;ARCH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;uname&lt;/span&gt; &lt;span class="nt"&gt;-m&lt;/span&gt; | &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s1"&gt;'s/x86_64/amd64/'&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s1"&gt;'s/\(arm\)\(64\)\?.*/\1\2/'&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s1"&gt;'s/aarch64$/arm64/'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nv"&gt;TAG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; https://api.github.com/repos/hashmap-kz/katomik/releases/latest | jq &lt;span class="nt"&gt;-r&lt;/span&gt; .tag_name&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

curl &lt;span class="nt"&gt;-L&lt;/span&gt; &lt;span class="s2"&gt;"https://github.com/hashmap-kz/katomik/releases/download/&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;TAG&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/katomik_&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;TAG&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;_&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;OS&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;_&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;ARCH&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.tar.gz"&lt;/span&gt; |
&lt;span class="nb"&gt;tar&lt;/span&gt; &lt;span class="nt"&gt;-xzf&lt;/span&gt; - &lt;span class="nt"&gt;-C&lt;/span&gt; /usr/local/bin &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nb"&gt;chmod&lt;/span&gt; +x /usr/local/bin/katomik
&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Homebrew installation
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;brew tap hashmap-kz/homebrew-tap
brew &lt;span class="nb"&gt;install &lt;/span&gt;katomik
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Usage
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Apply multiple files atomically&lt;/span&gt;
katomik apply &lt;span class="nt"&gt;-f&lt;/span&gt; manifests/

&lt;span class="c"&gt;# Read from stdin&lt;/span&gt;
katomik apply &lt;span class="nt"&gt;-f&lt;/span&gt; - &amp;lt; all.yaml

&lt;span class="c"&gt;# Apply recursively&lt;/span&gt;
katomik apply &lt;span class="nt"&gt;-R&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; ./deploy/

&lt;span class="c"&gt;# Set a custom timeout (default: 5m)&lt;/span&gt;
katomik apply &lt;span class="nt"&gt;--timeout&lt;/span&gt; 2m &lt;span class="nt"&gt;-f&lt;/span&gt; ./manifests/

&lt;span class="c"&gt;# Process and apply a manifest located on a remote server&lt;/span&gt;
katomik apply &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-f&lt;/span&gt; https://raw.githubusercontent.com/user/repo/refs/heads/master/manifests/deployment.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Example Output
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# katomik apply -f test/integration/k8s/manifests/

┌───────────────────────────────────────┬──────────────┐
│               RESOURCE                │  NAMESPACE   │
├───────────────────────────────────────┼──────────────┤
│ Namespace/katomik-test                │ (cluster)    │
│ ConfigMap/postgresql-init-script      │ katomik-test │
│ ConfigMap/postgresql-envs             │ katomik-test │
│ ConfigMap/postgresql-conf             │ katomik-test │
│ Service/postgres                      │ katomik-test │
│ PersistentVolumeClaim/postgres-data   │ katomik-test │
│ StatefulSet/postgres                  │ katomik-test │
│ ConfigMap/prometheus-config           │ katomik-test │
│ PersistentVolumeClaim/prometheus-data │ katomik-test │
│ Service/prometheus                    │ katomik-test │
│ StatefulSet/prometheus                │ katomik-test │
│ PersistentVolumeClaim/grafana-data    │ katomik-test │
│ Service/grafana                       │ katomik-test │
│ ConfigMap/grafana-datasources         │ katomik-test │
│ Deployment/grafana                    │ katomik-test │
└───────────────────────────────────────┴──────────────┘

+ watching
| Service/grafana                       katomik-test Unknown
| Deployment/grafana                    katomik-test Unknown
| StatefulSet/postgres                  katomik-test InProgress
| StatefulSet/prometheus                katomik-test InProgress
+ watching

✓ Success
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Quick Start
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cd test/integration/k8s
bash 00-setup-kind.sh
katomik apply -f manifests/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  🔒 Rollback Guarantees
&lt;/h2&gt;

&lt;p&gt;On failure (bad manifest, missing dependency, timeout, etc.):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Existing objects are reverted to their exact pre-apply state.&lt;/li&gt;
&lt;li&gt;New objects are deleted.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This guarantees your cluster remains consistent - no partial updates.&lt;/p&gt;




&lt;h2&gt;
  
  
  Flags
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Flag&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-f&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;File, directory, or &lt;code&gt;-&lt;/code&gt; for stdin&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-R&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Recurse into directories&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--timeout&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Timeout to wait for readiness&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Feedback
&lt;/h2&gt;

&lt;p&gt;Have a feature request or issue? Feel free to &lt;a href="https://github.com/hashmap-kz/katomik/issues" rel="noopener noreferrer"&gt;open an issue&lt;/a&gt;&lt;br&gt;
or submit a PR!&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>devops</category>
      <category>cicd</category>
      <category>go</category>
    </item>
  </channel>
</rss>
