<?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>Forem: kuro</title>
    <description>The latest articles on Forem by kuro (@kurorr).</description>
    <link>https://forem.com/kurorr</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%2F886044%2F0a584444-9c43-4404-be06-5b081ee8e195.gif</url>
      <title>Forem: kuro</title>
      <link>https://forem.com/kurorr</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/kurorr"/>
    <language>en</language>
    <item>
      <title>Stop Burning Snowflake Credits - Build a Local Emulator with Go and DuckDB</title>
      <dc:creator>kuro</dc:creator>
      <pubDate>Tue, 06 Jan 2026 14:16:12 +0000</pubDate>
      <link>https://forem.com/kurorr/stop-burning-snowflake-credits-build-a-local-emulator-with-go-and-duckdb-44lk</link>
      <guid>https://forem.com/kurorr/stop-burning-snowflake-credits-build-a-local-emulator-with-go-and-duckdb-44lk</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; &lt;a href="https://github.com/nnnkkk7/snowflake-emulator" rel="noopener noreferrer"&gt;snowflake-emulator&lt;/a&gt; is a local &lt;strong&gt;Snowflake-compatible SQL interface&lt;/strong&gt; powered by DuckDB, designed for dev/test. It works with the &lt;a href="https://github.com/snowflakedb/gosnowflake" rel="noopener noreferrer"&gt;gosnowflake&lt;/a&gt; driver (Go) and &lt;strong&gt;REST API v2&lt;/strong&gt; (Python, Node.js, any language). Free.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why I Built This
&lt;/h2&gt;

&lt;p&gt;I was building a data pipeline that talked to Snowflake. Running integration tests meant spinning up a warehouse, waiting for queries, and using credits.&lt;/p&gt;

&lt;p&gt;Mocking didn't help much either:&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;// This passes with mocks...&lt;/span&gt;
&lt;span class="n"&gt;mock&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ExpectQuery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"SELECT IFF(x &amp;gt; 0, 'yes', 'no')"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WillReturnRows&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;// ...but IFF doesn't exist in PostgreSQL.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Mocks don't catch SQL compatibility issues. I wanted something that actually executes Snowflake SQL locally.&lt;/p&gt;




&lt;h2&gt;
  
  
  Architecture: Executor as Orchestrator
&lt;/h2&gt;

&lt;p&gt;The emulator uses an orchestrator pattern where the &lt;strong&gt;Executor&lt;/strong&gt; coordinates SQL processing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                    ┌─────────────────────────────────────┐
                    │             Executor                │
                    │  ┌───────────┐   ┌────────────┐     │
   SQL Request ───&amp;gt; │  │ Classifier│   │ Translator │     │ ───&amp;gt; DuckDB
                    │  └───────────┘   └────────────┘     │
                    │       │                │            │
                    │    Detect          Transform        │
                    │    statement       Snowflake SQL    │
                    │    type            to DuckDB        │
                    └─────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Executor&lt;/strong&gt; receives SQL and orchestrates the processing pipeline&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Classifier&lt;/strong&gt; detects statement type (SELECT, DDL, COPY INTO, MERGE)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Translator&lt;/strong&gt; transforms Snowflake-specific syntax to DuckDB&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Executor&lt;/strong&gt; runs the translated SQL on DuckDB&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Key Design: AST-Based SQL Translation
&lt;/h2&gt;

&lt;p&gt;The most interesting part is the SQL translation. I tried regex first:&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;// Bad: Regex breaks on edge cases&lt;/span&gt;
&lt;span class="n"&gt;sql&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;regexp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MustCompile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;`\bIFF\s*\(`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ReplaceAllString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"IF("&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;// What about: SELECT 'IFF(' AS label?&lt;/span&gt;
&lt;span class="c"&gt;// → SELECT 'IF(' AS label  ← Broken!&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Regex can't distinguish function calls from string literals. So I switched to AST-based parsing using &lt;a href="https://github.com/blastrain/vitess-sqlparser" rel="noopener noreferrer"&gt;vitess-sqlparser&lt;/a&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;// Good: AST transformation is precise&lt;/span&gt;
&lt;span class="n"&gt;sqlparser&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Walk&lt;/span&gt;&lt;span class="p"&gt;(&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;node&lt;/span&gt; &lt;span class="n"&gt;sqlparser&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SQLNode&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;bool&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="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;fn&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;node&lt;/span&gt;&lt;span class="o"&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;sqlparser&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FuncExpr&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;ok&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;strings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;EqualFold&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;Name&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="s"&gt;"IFF"&lt;/span&gt;&lt;span class="p"&gt;)&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;Name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sqlparser&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewColIdent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"IF"&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="no"&gt;true&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="n"&gt;stmt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;AST parsing correctly identifies function nodes vs string literals, so &lt;code&gt;SELECT 'IFF(' AS label&lt;/code&gt; stays unchanged.&lt;/p&gt;

&lt;p&gt;In the actual implementation, simple renames (e.g., &lt;code&gt;IFF&lt;/code&gt; → &lt;code&gt;IF&lt;/code&gt;) are handled via a function map, and more complex cases use small handlers plus a post-processing pass.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Marker Function Pattern
&lt;/h2&gt;

&lt;p&gt;Some transformations can't be done in-place. For example, &lt;code&gt;DATEADD(day, 7, date)&lt;/code&gt; needs to become &lt;code&gt;date + INTERVAL 7 day&lt;/code&gt; — the arguments need to be reordered.&lt;/p&gt;

&lt;p&gt;I use a two-stage approach:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stage 1: Mark during AST walk&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;// DATEADD(day, 7, created_at) → __DATEADD__('day', 7, created_at)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;strings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;EqualFold&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;Name&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="s"&gt;"DATEADD"&lt;/span&gt;&lt;span class="p"&gt;)&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;Name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sqlparser&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewColIdent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"__DATEADD__"&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;Stage 2: Post-process with a small parser (no regex)&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;// __DATEADD__(part, n, date) → (CAST(date AS DATE) + interval n part)&lt;/span&gt;
&lt;span class="n"&gt;sql&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;transformMarkedFunction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"__DATEADD__"&lt;/span&gt;&lt;span class="p"&gt;,&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;args&lt;/span&gt; &lt;span class="kt"&gt;string&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;parts&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;splitFunctionArgs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c"&gt;// respects nested parentheses&lt;/span&gt;
    &lt;span class="n"&gt;part&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;strings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TrimSpace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;parts&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;n&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;strings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TrimSpace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;parts&lt;/span&gt;&lt;span class="p"&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;date&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;strings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TrimSpace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;parts&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="k"&gt;return&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;Sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"(CAST(%s AS DATE) + interval %s %s)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;part&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 marker function (&lt;code&gt;__DATEADD__&lt;/code&gt;) is a placeholder that's safe to transform after AST serialization (it only exists because we inserted it during the AST walk). Using a tiny parser keeps it robust when arguments contain nested expressions (e.g., function calls), without relying on brittle regexes.&lt;/p&gt;




&lt;h2&gt;
  
  
  DuckDB Connection Handling
&lt;/h2&gt;

&lt;p&gt;DuckDB only supports a single writer at a time. Multiple readers are fine, but writes must be serialized.&lt;/p&gt;

&lt;p&gt;I solved this with a simple mutex pattern:&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;Manager&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;db&lt;/span&gt;      &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DB&lt;/span&gt;
    &lt;span class="n"&gt;writeMu&lt;/span&gt; &lt;span class="n"&gt;sync&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Mutex&lt;/span&gt;  &lt;span class="c"&gt;// Serializes write operations&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;// Reads can be concurrent (no lock)&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;m&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;Manager&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;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;query&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;args&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="n"&gt;any&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;sql&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Rows&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="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;m&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;QueryContext&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;args&lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;// Writes are serialized&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;m&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;Manager&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;Exec&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;query&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;args&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="n"&gt;any&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Result&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;m&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;writeMu&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Lock&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;writeMu&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Unlock&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;m&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;ExecContext&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;args&lt;/span&gt;&lt;span class="o"&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;This lives in &lt;code&gt;pkg/connection/manager.go&lt;/code&gt;. When an operation needs to touch both DuckDB and the emulator's metadata tables (e.g., &lt;code&gt;CREATE DATABASE&lt;/code&gt; / &lt;code&gt;CREATE SCHEMA&lt;/code&gt;), it uses &lt;code&gt;ExecTx&lt;/code&gt; to keep those changes atomic. (Table-level metadata tracking for &lt;code&gt;CREATE TABLE&lt;/code&gt; / &lt;code&gt;DROP TABLE&lt;/code&gt; is intentionally kept minimal for now.)&lt;/p&gt;




&lt;h2&gt;
  
  
  Quick Start
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;-p&lt;/span&gt; 8080:8080 ghcr.io/nnnkkk7/snowflake-emulator:latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Optional: run with persistent storage:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;-p&lt;/span&gt; 8080:8080 &lt;span class="nt"&gt;-v&lt;/span&gt; snowflake-data:/data &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;DB_PATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/data/snowflake.db &lt;span class="se"&gt;\&lt;/span&gt;
  ghcr.io/nnnkkk7/snowflake-emulator:latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Go (gosnowflake driver):&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="n"&gt;dsn&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="s"&gt;"user:pass@localhost:8080/TEST_DB/PUBLIC?account=test&amp;amp;protocol=http"&lt;/span&gt;
&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"snowflake"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dsn&lt;/span&gt;&lt;span class="p"&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;Query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;`SELECT IFF(score &amp;gt;= 90, 'A', 'B'), DATEADD(day, 7, created_at) FROM users`&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;Python, Node.js, etc. (REST API v2):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:8080/api/v2/statements &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"statement": "SELECT IFF(1 &amp;gt; 0, '&lt;/span&gt;&lt;span class="se"&gt;\'&lt;/span&gt;&lt;span class="s1"&gt;'yes'&lt;/span&gt;&lt;span class="se"&gt;\'&lt;/span&gt;&lt;span class="s1"&gt;', '&lt;/span&gt;&lt;span class="se"&gt;\'&lt;/span&gt;&lt;span class="s1"&gt;'no'&lt;/span&gt;&lt;span class="se"&gt;\'&lt;/span&gt;&lt;span class="s1"&gt;')", "database": "TEST_DB"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The response includes a &lt;code&gt;statementHandle&lt;/code&gt;. Fetch the result via &lt;code&gt;GET /api/v2/statements/{handle}&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Limitations
&lt;/h2&gt;

&lt;p&gt;This is a dev/test tool, not a Snowflake replacement:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No authentication (dev mode only)&lt;/li&gt;
&lt;li&gt;No Time Travel / Zero-Copy Cloning&lt;/li&gt;
&lt;li&gt;No external stages (S3, Azure, GCS)&lt;/li&gt;
&lt;li&gt;No stored procedures / UDFs&lt;/li&gt;
&lt;/ul&gt;




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

&lt;p&gt;Building a SQL translator taught me that regex is tempting but fragile for SQL transformations. AST-based parsing handles edge cases correctly, and the marker function pattern bridges the gap when in-place AST transformations aren't possible.&lt;/p&gt;

&lt;p&gt;Repository: &lt;a href="https://github.com/nnnkkk7/snowflake-emulator" rel="noopener noreferrer"&gt;github.com/nnnkkk7/snowflake-emulator&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you try it, let me know what SQL functions are missing.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The Go gopher was designed by Renee French.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>go</category>
      <category>duckdb</category>
      <category>snowflake</category>
      <category>docker</category>
    </item>
    <item>
      <title>gopin - Automate Version Pinning for Go Install Commands</title>
      <dc:creator>kuro</dc:creator>
      <pubDate>Mon, 01 Dec 2025 14:40:33 +0000</pubDate>
      <link>https://forem.com/kurorr/gopin-automate-version-pinning-for-go-install-commands-46m</link>
      <guid>https://forem.com/kurorr/gopin-automate-version-pinning-for-go-install-commands-46m</guid>
      <description>&lt;p&gt;Have you ever wondered why your CI pipeline suddenly broke even though you didn't change any code? Or why a teammate's local build produces different results than yours? The culprit might be lurking in your &lt;code&gt;go install&lt;/code&gt; commands with &lt;code&gt;@latest&lt;/code&gt; tags.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/nnnkkk7/gopin" rel="noopener noreferrer"&gt;gopin&lt;/a&gt;&lt;/strong&gt; is a CLI tool that automatically pins versions of &lt;code&gt;go install&lt;/code&gt; commands in your codebase, ensuring reproducible builds and enhanced security.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem with &lt;code&gt;@latest&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Using &lt;code&gt;@latest&lt;/code&gt; in &lt;code&gt;go install&lt;/code&gt; commands creates several issues:&lt;/p&gt;

&lt;h3&gt;
  
  
  Reproducibility Issues
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight make"&gt;&lt;code&gt;&lt;span class="nl"&gt;.PHONY&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;lint&lt;/span&gt;
&lt;span class="nl"&gt;lint&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
    go &lt;span class="nb"&gt;install &lt;/span&gt;github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest
    golangci-lint run
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Today it installs v2.6.0, tomorrow it might install v2.6.2. Builds become non-deterministic.&lt;/p&gt;

&lt;h3&gt;
  
  
  Security Risk
&lt;/h3&gt;

&lt;p&gt;Unpinned versions increase supply chain attack risk. A compromised version could be installed without your knowledge.&lt;/p&gt;

&lt;h3&gt;
  
  
  CI/CD Instability
&lt;/h3&gt;

&lt;p&gt;Different runners may install different versions, causing inconsistent test results and build failures.&lt;/p&gt;

&lt;h3&gt;
  
  
  Debugging Difficulty
&lt;/h3&gt;

&lt;p&gt;Reproducing the exact environment from weeks or months ago becomes impossible with &lt;code&gt;@latest&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Introducing gopin
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/nnnkkk7/gopin" rel="noopener noreferrer"&gt;gopin&lt;/a&gt;&lt;/strong&gt; is a CLI tool that solves these problems by automatically updating all &lt;code&gt;go install&lt;/code&gt; commands to the latest specific semantic versions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pin &lt;code&gt;@latest&lt;/code&gt;&lt;/strong&gt;: Convert &lt;code&gt;@latest&lt;/code&gt; to specific versions (e.g., &lt;code&gt;@v2.6.2&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Update outdated versions&lt;/strong&gt;: Update already-pinned versions to the latest (e.g., &lt;code&gt;@v2.0.0&lt;/code&gt; → &lt;code&gt;@v2.6.2&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add missing versions&lt;/strong&gt;: Add version specifiers to commands without them
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Example 1: Pin @latest&lt;/span&gt;
go &lt;span class="nb"&gt;install &lt;/span&gt;github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest
&lt;span class="c"&gt;# → go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.6.2&lt;/span&gt;

&lt;span class="c"&gt;# Example 2: Update outdated version&lt;/span&gt;
go &lt;span class="nb"&gt;install &lt;/span&gt;github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.0.0
&lt;span class="c"&gt;# → go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.6.2&lt;/span&gt;

&lt;span class="c"&gt;# Example 3: Add missing version&lt;/span&gt;
go &lt;span class="nb"&gt;install &lt;/span&gt;github.com/air-verse/air
&lt;span class="c"&gt;# → go install github.com/air-verse/air@v1.61.7&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Architecture: A Three-Stage Pipeline
&lt;/h2&gt;

&lt;p&gt;gopin follows a clean, modular architecture with three core stages:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────┐      ┌─────────────┐      ┌─────────────┐
│  Detector   │ ───&amp;gt; │  Resolver   │ ───&amp;gt; │  Rewriter   │
└─────────────┘      └─────────────┘      └─────────────┘
     │                     │                     │
     │                     │                     │
  Scan files         Query versions         Replace text
  with regex         from proxy.golang.org  in-place
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  1. Detection Phase
&lt;/h3&gt;

&lt;p&gt;The detector scans files for &lt;code&gt;go install&lt;/code&gt; patterns using regex:&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;// From pkg/detector/detector.go&lt;/span&gt;
&lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;GoInstallPattern&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;regexp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MustCompile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;`go\s+install\s+`&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;                  &lt;span class="c"&gt;// go install&lt;/span&gt;
    &lt;span class="s"&gt;`(?:-[a-zA-Z0-9_=,]+\s+)*`&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;         &lt;span class="c"&gt;// Optional flags&lt;/span&gt;
    &lt;span class="s"&gt;`([^\s@#]+)`&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;                       &lt;span class="c"&gt;// Module path&lt;/span&gt;
    &lt;span class="s"&gt;`(?:@([^\s#]+))?`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                   &lt;span class="c"&gt;// Version (optional)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This pattern handles various edge cases:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Flags: &lt;code&gt;go install -v -trimpath github.com/tool@latest&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Subpackages: &lt;code&gt;github.com/org/repo/cmd/tool@latest&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;No version: &lt;code&gt;go install github.com/tool&lt;/code&gt; (implicitly @latest)&lt;/li&gt;
&lt;li&gt;Special characters: &lt;code&gt;gopkg.in/yaml.v3@latest&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. Resolution Phase
&lt;/h3&gt;

&lt;p&gt;The resolver queries &lt;code&gt;proxy.golang.org&lt;/code&gt; for the latest version:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GET https://proxy.golang.org/github.com/golangci/golangci-lint/v2/@latest

Response:
{
  "Version": "v2.6.2",
  "Time": "2024-12-01T10:30:00Z"
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Key Design: Resolver Chain Pattern&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Resolvers are composed using the decorator pattern:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CachedResolver
    → FallbackResolver
        → ProxyResolver (primary)
        → GoListResolver (fallback)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This design provides:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Caching for repeated module lookups&lt;/li&gt;
&lt;li&gt;Fallback to &lt;code&gt;go list&lt;/code&gt; for private modules&lt;/li&gt;
&lt;li&gt;Flexibility to add new resolution strategies
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// From pkg/cli/app.go&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;createResolver&lt;/span&gt;&lt;span class="p"&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;config&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;resolver&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Resolver&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;res&lt;/span&gt; &lt;span class="n"&gt;resolver&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Resolver&lt;/span&gt;

    &lt;span class="k"&gt;switch&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;Resolver&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Type&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;"golist"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;resolver&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewGoListResolver&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;resolver&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewProxyResolver&lt;/span&gt;&lt;span class="p"&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;Resolver&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ProxyURL&lt;/span&gt;&lt;span class="p"&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;Resolver&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Timeout&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Resolver&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fallback&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;resolver&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewFallbackResolver&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;resolver&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewGoListResolver&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;resolver&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewCachedResolver&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;res&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;h3&gt;
  
  
  3. Rewriting Phase
&lt;/h3&gt;

&lt;p&gt;The rewriter replaces version strings in-place with change tracking.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key Design: Backward Processing&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Matches are processed in reverse order (last line first, rightmost column first) to prevent offset shifts:&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;// From pkg/rewriter/rewriter.go&lt;/span&gt;
&lt;span class="n"&gt;sort&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;matches&lt;/span&gt;&lt;span class="p"&gt;,&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;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;bool&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;matches&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;Line&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;matches&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;Line&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;matches&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;Line&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;matches&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;Line&lt;/span&gt;  &lt;span class="c"&gt;// Descending&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;matches&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;StartColumn&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;matches&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;StartColumn&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This prevents offset shifts - modifying line 5 doesn't affect line 10's position. Forward processing would require recalculating offsets after each change.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real-World Example
&lt;/h2&gt;

&lt;p&gt;Let's see gopin in action with a typical Makefile:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Before:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight make"&gt;&lt;code&gt;&lt;span class="nl"&gt;.PHONY&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;install-tools&lt;/span&gt;
&lt;span class="nl"&gt;install-tools&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
    go &lt;span class="nb"&gt;install &lt;/span&gt;github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest
    go &lt;span class="nb"&gt;install &lt;/span&gt;golang.org/x/tools/cmd/goimports@latest
    go &lt;span class="nb"&gt;install &lt;/span&gt;honnef.co/go/tools/cmd/staticcheck@latest
    go &lt;span class="nb"&gt;install &lt;/span&gt;github.com/securego/gosec/v2/cmd/gosec@latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Running gopin:&lt;/strong&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="nv"&gt;$ &lt;/span&gt;gopin run &lt;span class="nt"&gt;--diff&lt;/span&gt;

&lt;span class="nt"&gt;---&lt;/span&gt; Makefile
+++ Makefile
- go &lt;span class="nb"&gt;install &lt;/span&gt;github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest
+ go &lt;span class="nb"&gt;install &lt;/span&gt;github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.6.2
- go &lt;span class="nb"&gt;install &lt;/span&gt;golang.org/x/tools/cmd/goimports@latest
+ go &lt;span class="nb"&gt;install &lt;/span&gt;golang.org/x/tools/cmd/goimports@v0.39.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;After:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight make"&gt;&lt;code&gt;&lt;span class="nl"&gt;.PHONY&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;install-tools&lt;/span&gt;
&lt;span class="nl"&gt;install-tools&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
    go &lt;span class="nb"&gt;install &lt;/span&gt;github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.6.2
    go &lt;span class="nb"&gt;install &lt;/span&gt;golang.org/x/tools/cmd/goimports@v0.39.0
    go &lt;span class="nb"&gt;install &lt;/span&gt;honnef.co/go/tools/cmd/staticcheck@v0.5.1
    go &lt;span class="nb"&gt;install &lt;/span&gt;github.com/securego/gosec/v2/cmd/gosec@v2.22.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Default Target Files
&lt;/h2&gt;

&lt;p&gt;By default, gopin scans these file patterns:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;.github/**/*.yml&lt;/code&gt; and &lt;code&gt;.github/**/*.yaml&lt;/code&gt; - GitHub Actions workflows&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Makefile&lt;/code&gt;, &lt;code&gt;makefile&lt;/code&gt;, &lt;code&gt;GNUmakefile&lt;/code&gt; - Make build files&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;*.mk&lt;/code&gt; - Make include files&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can customize target files using a &lt;code&gt;.gopin.yaml&lt;/code&gt; configuration file.&lt;/p&gt;

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

&lt;p&gt;Version pinning helps ensure reproducible builds and reduces security risks. gopin automates this process for &lt;code&gt;go install&lt;/code&gt; commands across your codebase using a clean three-stage architecture: detection, resolution, and rewriting.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Get started:&lt;/strong&gt;&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/nnnkkk7/gopin/cmd/gopin@latest
&lt;span class="nb"&gt;cd &lt;/span&gt;your-project
gopin run &lt;span class="nt"&gt;--dry-run&lt;/span&gt;  &lt;span class="c"&gt;# Preview changes&lt;/span&gt;
gopin run            &lt;span class="c"&gt;# Apply changes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;The Go gopher was designed by Renee French.&lt;/p&gt;

</description>
      <category>go</category>
      <category>devops</category>
      <category>security</category>
    </item>
  </channel>
</rss>
