<?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: Mulaimu00</title>
    <description>The latest articles on DEV Community by Mulaimu00 (@mulaimu00).</description>
    <link>https://dev.to/mulaimu00</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4069788%2Fd71b9470-96c1-421a-893c-4eecd86fb55d.png</url>
      <title>DEV Community: Mulaimu00</title>
      <link>https://dev.to/mulaimu00</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mulaimu00"/>
    <language>en</language>
    <item>
      <title>PostgreSQL interval and Go: The Runtime Trap Nobody Warns You About</title>
      <dc:creator>Mulaimu00</dc:creator>
      <pubDate>Sun, 09 Aug 2026 11:51:04 +0000</pubDate>
      <link>https://dev.to/mulaimu00/postgresql-interval-and-go-the-runtime-trap-nobody-warns-you-about-5gc8</link>
      <guid>https://dev.to/mulaimu00/postgresql-interval-and-go-the-runtime-trap-nobody-warns-you-about-5gc8</guid>
      <description>&lt;p&gt;While building a Go application backed by PostgreSQL, I made a database design decision that seemed perfectly reasonable until it wasn't. This is the story of that mistake, why it happened, and the right way to handle durations across PostgreSQL and Go.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Setup
&lt;/h2&gt;

&lt;p&gt;I needed a table to store items with a duration field  how long something lasts. Simple enough. My first instinct was to use PostgreSQL's &lt;code&gt;interval&lt;/code&gt; type:&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;packages&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;id&lt;/span&gt;         &lt;span class="n"&gt;bigserial&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;name&lt;/span&gt;       &lt;span class="nb"&gt;text&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;duration&lt;/span&gt;   &lt;span class="n"&gt;interval&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;price&lt;/span&gt;      &lt;span class="nb"&gt;integer&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;active&lt;/span&gt;     &lt;span class="nb"&gt;boolean&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="n"&gt;timestamptz&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;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;now&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 in Go, I mapped it to &lt;code&gt;time.Duration&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;Package&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;ID&lt;/span&gt;        &lt;span class="kt"&gt;int64&lt;/span&gt;
    &lt;span class="n"&gt;Name&lt;/span&gt;      &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="n"&gt;Duration&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;Duration&lt;/span&gt;
    &lt;span class="n"&gt;Price&lt;/span&gt;     &lt;span class="kt"&gt;int64&lt;/span&gt;
    &lt;span class="n"&gt;Active&lt;/span&gt;    &lt;span class="kt"&gt;bool&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This looks correct. PostgreSQL has a native duration type. Go has a native duration type. They should map cleanly, right?&lt;/p&gt;

&lt;h2&gt;
  
  
  The Runtime Surprise
&lt;/h2&gt;

&lt;p&gt;Everything compiled. The server started. Then I hit the endpoint that queried the table:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sql: Scan error on column index 2, name "duration": converting driver.Value type []uint8 ("01:00:00") to int64: invalid syntax
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Go PostgreSQL driver (&lt;code&gt;lib/pq&lt;/code&gt;) has no built-in conversion between PostgreSQL &lt;code&gt;interval&lt;/code&gt; and Go &lt;code&gt;time.Duration&lt;/code&gt;. The driver receives the interval as raw bytes (&lt;code&gt;"01:00:00"&lt;/code&gt;) and tries to stuff it into an &lt;code&gt;int64&lt;/code&gt;  which is what &lt;code&gt;time.Duration&lt;/code&gt; is under the hood. It fails silently at runtime, not at compile time.&lt;/p&gt;

&lt;p&gt;This is the dangerous kind of bug. It compiles perfectly. It only surfaces when real data flows through the system.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Happens
&lt;/h2&gt;

&lt;p&gt;PostgreSQL's &lt;code&gt;interval&lt;/code&gt; is a complex type. It can represent:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;"1 hour"&lt;/code&gt; → &lt;code&gt;01:00:00&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;"24 hours"&lt;/code&gt; → &lt;code&gt;24:00:00&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;"1 day"&lt;/code&gt; → &lt;code&gt;1 day&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;"1 month"&lt;/code&gt; → ambiguous (30 days? 31 days?)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Go's &lt;code&gt;time.Duration&lt;/code&gt; is simply an &lt;code&gt;int64&lt;/code&gt; counting nanoseconds. There is no universal, lossless conversion between these two representations  especially for month/year intervals. So &lt;code&gt;lib/pq&lt;/code&gt; doesn't try.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Right Solution
&lt;/h2&gt;

&lt;p&gt;Store duration as integer seconds in PostgreSQL:&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;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;packages&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="n"&gt;duration&lt;/span&gt; &lt;span class="k"&gt;TYPE&lt;/span&gt; &lt;span class="nb"&gt;integer&lt;/span&gt;
&lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="k"&gt;EXTRACT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;EPOCH&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="nb"&gt;integer&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Update the Go model:&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;Package&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;ID&lt;/span&gt;        &lt;span class="kt"&gt;int64&lt;/span&gt;
    &lt;span class="n"&gt;Name&lt;/span&gt;      &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="n"&gt;Duration&lt;/span&gt;  &lt;span class="kt"&gt;int64&lt;/span&gt; &lt;span class="c"&gt;// seconds&lt;/span&gt;
    &lt;span class="n"&gt;Price&lt;/span&gt;     &lt;span class="kt"&gt;int64&lt;/span&gt;
    &lt;span class="n"&gt;Active&lt;/span&gt;    &lt;span class="kt"&gt;bool&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now 1 hour = &lt;code&gt;3600&lt;/code&gt;, 24 hours = &lt;code&gt;86400&lt;/code&gt;. Dead simple.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Integer Seconds Is The Right Call
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Arithmetic is trivial:&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;expiresAt&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;startedAt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&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;Duration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;seconds&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&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;Second&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;No parsing ambiguity.&lt;/strong&gt; &lt;code&gt;"01:00:00"&lt;/code&gt;, &lt;code&gt;"1 hour"&lt;/code&gt;, &lt;code&gt;"3600 seconds"&lt;/code&gt; are all valid PostgreSQL intervals. Integer seconds has exactly one representation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Industry standard.&lt;/strong&gt; Stripe stores subscription intervals as integers. Unix timestamps are integers. The payments and infrastructure world converged on integers for time values decades ago for good reason.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Easy to display:&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;hours&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;seconds&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="m"&gt;3600&lt;/span&gt;
&lt;span class="n"&gt;minutes&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;seconds&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="m"&gt;3600&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="m"&gt;60&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The migration is safe.&lt;/strong&gt; &lt;code&gt;EXTRACT(EPOCH FROM duration)&lt;/code&gt; converts interval to seconds with no data loss for hour/day durations.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Broader Lesson
&lt;/h2&gt;

&lt;p&gt;This bug exposed a general principle: don't assume that similar concepts in two systems map cleanly to each other.&lt;/p&gt;

&lt;p&gt;PostgreSQL &lt;code&gt;interval&lt;/code&gt; and Go &lt;code&gt;time.Duration&lt;/code&gt; are both "duration types" conceptually. But they live in different layers of the stack — database and application — with a driver in between that has to translate between them. When that translation isn't implemented, you get a runtime error instead of a compile error.&lt;/p&gt;

&lt;p&gt;The safe approach: use primitive types at database boundaries. Integers, strings, booleans. Let the application layer handle the semantics.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Building in Go, learning in public. Follow along for more.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>go</category>
      <category>postgressql</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
  </channel>
</rss>
