<?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: Mikhail Shytsko</title>
    <description>The latest articles on DEV Community by Mikhail Shytsko (@mikh-shytsko).</description>
    <link>https://dev.to/mikh-shytsko</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%2F3948102%2Fa736e791-77e9-40e8-b6df-1059059f6f5e.jpg</url>
      <title>DEV Community: Mikhail Shytsko</title>
      <link>https://dev.to/mikh-shytsko</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mikh-shytsko"/>
    <language>en</language>
    <item>
      <title>The Postgres Insert That Fails Right After a Successful Load</title>
      <dc:creator>Mikhail Shytsko</dc:creator>
      <pubDate>Wed, 26 Aug 2026 12:00:33 +0000</pubDate>
      <link>https://dev.to/mikh-shytsko/the-postgres-insert-that-fails-right-after-a-successful-load-20dm</link>
      <guid>https://dev.to/mikh-shytsko/the-postgres-insert-that-fails-right-after-a-successful-load-20dm</guid>
      <description>&lt;p&gt;The load finished without complaint, with row counts matching the fixture file and every foreign key resolving, but then the application inserts a row of its own, and Postgres refuses it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ERROR:  duplicate key value violates unique constraint "users_pkey"
DETAIL:  Key (id)=(1) already exists.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing is corrupt and nothing needs restoring. What you do have is a Postgres sequence out of sync with the table it feeds, the most common way a clean data load leaves a database broken, and the mechanism behind it is almost disappointingly plain, because writing an explicit &lt;code&gt;id&lt;/code&gt; never tells the sequence that the value has been taken.&lt;/p&gt;

&lt;p&gt;Everything below was run against PostgreSQL 18.6 in a throwaway container on 2026-08-21, and the outputs are pasted as they came back.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Explicit ids and generated ids come from two different places, and loading the former doesn't move the latter.&lt;/li&gt;
&lt;li&gt;Moving from &lt;code&gt;serial&lt;/code&gt; to an identity column changes nothing about this. &lt;code&gt;GENERATED ALWAYS&lt;/code&gt; at least refuses the load outright, but add &lt;code&gt;OVERRIDING SYSTEM VALUE&lt;/code&gt; to get past it and you inherit the same stale sequence.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;pg_get_serial_sequence()&lt;/code&gt; resolves the sequence behind a column for both &lt;code&gt;serial&lt;/code&gt; and identity, which matters because a sequence keeps its original name when the table is renamed.&lt;/li&gt;
&lt;li&gt;On an empty table the popular &lt;code&gt;setval(seq, max(id))&lt;/code&gt; recipe quietly does nothing at all, since &lt;code&gt;setval&lt;/code&gt; handed a NULL returns without acting.&lt;/li&gt;
&lt;li&gt;Whether the number you pass to &lt;code&gt;setval&lt;/code&gt; is the next value or the last one used comes down to the &lt;code&gt;is_called&lt;/code&gt; flag. Get it backwards and you lose exactly one id.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why the sequence goes out of sync
&lt;/h2&gt;

&lt;p&gt;A &lt;code&gt;bigserial&lt;/code&gt; column is really a &lt;code&gt;bigint&lt;/code&gt; carrying a default of &lt;code&gt;nextval('&amp;lt;sequence&amp;gt;')&lt;/code&gt;, so supplying your own value in the &lt;code&gt;INSERT&lt;/code&gt; means that default is never evaluated at all, and the sequence sits where it was while the table fills up around it.&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;users&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;email&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="k"&gt;UNIQUE&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;users&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="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'a@example.com'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'b@example.com'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'c@example.com'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;last_value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;is_called&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;users_id_seq&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt; &lt;span class="n"&gt;last_value&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;is_called&lt;/span&gt;
&lt;span class="c1"&gt;------------+-----------&lt;/span&gt;
          &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;row&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5liukycovl2g031wmei3.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5liukycovl2g031wmei3.png" alt="Three rows loaded with explicit ids while the Postgres sequence out of sync behind them still reports last_value 1, so the next generated id repeats 1 and collides" width="800" height="434"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Three rows in the table, and the sequence still reports the state it had at creation, &lt;code&gt;last_value&lt;/code&gt; of 1 with &lt;code&gt;is_called&lt;/code&gt; false, which together mean that 1 has not been handed out yet. The next insert that lets Postgres pick the id therefore asks for 1, and 1 belongs to the fixture row you loaded a second ago.&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;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'d@example.com'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ERROR:  duplicate key value violates unique constraint "users_pkey"
DETAIL:  Key (id)=(1) already exists.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The failure surfaces late, which is what makes it confusing. Your seed ran green and so did CI, and the error then waits for the first write that a human or a test actually performs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Identity columns do not save you
&lt;/h2&gt;

&lt;p&gt;Since Postgres 10 the standard-conforming spelling is an identity column, and teams that moved off &lt;code&gt;serial&lt;/code&gt; sometimes assume the problem moved with it, which it didn't.&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;t_ident&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="nb"&gt;bigint&lt;/span&gt; &lt;span class="k"&gt;GENERATED&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;IDENTITY&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;x&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;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;t_ident&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="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'a'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'b'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;t_ident&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'c'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ERROR:  duplicate key value violates unique constraint "t_ident_pkey"
DETAIL:  Key (id)=(1) already exists.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;GENERATED ALWAYS&lt;/code&gt; is stricter and, for once, the strictness is useful, because it turns a silent trap into an immediate complaint with the workaround printed underneath:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ERROR:  cannot insert a non-DEFAULT value into column "id"
DETAIL:  Column "id" is an identity column defined as GENERATED ALWAYS.
HINT:  Use OVERRIDING SYSTEM VALUE to override.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Take the hint, and the load succeeds. So does the collision that follows it, because &lt;code&gt;OVERRIDING SYSTEM VALUE&lt;/code&gt; only suspends the check that blocks your value from being written and has nothing at all to say about the sequence underneath, which stays parked at 1 while rows 1 and 2 go in.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one-line fix, and two ways it is usually written wrong
&lt;/h2&gt;

&lt;p&gt;Point the sequence at the largest value the table currently holds:&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;SELECT&lt;/span&gt; &lt;span class="n"&gt;setval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pg_get_serial_sequence&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'users'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'id'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;max&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;FROM&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt; &lt;span class="n"&gt;setval&lt;/span&gt;
&lt;span class="c1"&gt;--------&lt;/span&gt;
      &lt;span class="mi"&gt;3&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;row&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The next insert now returns id 4 and the incident is over, though two details in that line are worth more than the line itself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do not hardcode the sequence name!&lt;/strong&gt;&amp;nbsp;Nearly every version of this snippet on the internet writes &lt;code&gt;users_id_seq&lt;/code&gt; directly, which is correct until somebody renames the table. Sequences don't follow:&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;users&lt;/span&gt; &lt;span class="k"&gt;RENAME&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;members&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;pg_get_serial_sequence&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'members'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'id'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt; &lt;span class="n"&gt;pg_get_serial_sequence&lt;/span&gt;
&lt;span class="c1"&gt;------------------------&lt;/span&gt;
 &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;users_id_seq&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;row&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The table is &lt;code&gt;members&lt;/code&gt;, the sequence is still &lt;code&gt;users_id_seq&lt;/code&gt;, and a seed script that builds the name by string concatenation now targets a sequence that has nothing to do with the table it thinks it is fixing. &lt;code&gt;pg_get_serial_sequence()&lt;/code&gt; asks the catalog instead of guessing, and it answers for identity columns too, despite the "serial" in its name.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Watch the empty table.&lt;/strong&gt; Over zero rows &lt;code&gt;max(id)&lt;/code&gt; is NULL, and because &lt;code&gt;setval&lt;/code&gt; is strict, handing it a NULL means the call comes back without touching anything:&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;SELECT&lt;/span&gt; &lt;span class="n"&gt;setval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pg_get_serial_sequence&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'empty_t'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'id'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;max&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;FROM&lt;/span&gt; &lt;span class="n"&gt;empty_t&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt; &lt;span class="n"&gt;setval&lt;/span&gt;
&lt;span class="c1"&gt;--------&lt;/span&gt;

&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;row&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Postgres neither errors nor changes anything, which is harmless on a freshly created sequence and quietly wrong on one that an earlier run already advanced, the situation you're in whenever a suite truncates tables between runs. The form that survives both cases carries its own &lt;code&gt;is_called&lt;/code&gt; argument:&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;SELECT&lt;/span&gt; &lt;span class="n"&gt;setval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pg_get_serial_sequence&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'empty2'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'id'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;coalesce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;max&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="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="k"&gt;max&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;IS&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="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;empty2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Where rows exist, &lt;code&gt;max(id)&lt;/code&gt; becomes the last value used and the flag is true. An empty table takes the other branch, landing the sequence on 1 with the flag false, so the very next &lt;code&gt;nextval&lt;/code&gt; hands out 1 rather than 2. If you have ever wondered why a reset left you starting at 2, that flag is the reason, and the difference shows up in a two-line experiment:&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;SELECT&lt;/span&gt; &lt;span class="n"&gt;setval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'flagcheck_id_seq'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;        &lt;span class="c1"&gt;-- next insert gets 11&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;setval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'flagcheck_id_seq'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;-- next insert gets 10&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Resetting an identity column on its own terms
&lt;/h2&gt;

&lt;p&gt;Identity columns have native syntax that never touches a sequence name:&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;t_ident&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;id&lt;/span&gt; &lt;span class="k"&gt;RESTART&lt;/span&gt; &lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The next insert returns 3. Compared with &lt;code&gt;setval&lt;/code&gt; it reads better and gets checked at parse time, though it covers identity columns only, so pointing it at a &lt;code&gt;serial&lt;/code&gt; column tells you so plainly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ERROR:  column "id" of relation "users" is not an identity column
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Which of the two you reach for is mostly a question of how the table is declared, though the &lt;code&gt;setval&lt;/code&gt; form works on both kinds, which is worth something to a script that has to fix a whole schema without branching. When the table is disposable rather than seeded, &lt;code&gt;TRUNCATE t_ident RESTART IDENTITY&lt;/code&gt; empties it and rewinds the sequence to 1 in a single statement, for &lt;code&gt;serial&lt;/code&gt; and identity alike.&lt;/p&gt;

&lt;h2&gt;
  
  
  Realigning a whole schema after a load
&lt;/h2&gt;

&lt;p&gt;Fixing one table by hand is fine for an incident. After a bulk load into a schema of any size you want every affected sequence found and moved without naming any of them, and the catalog knows enough to do that on its own:&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;DO&lt;/span&gt; &lt;span class="err"&gt;$$&lt;/span&gt;
&lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;BEGIN&lt;/span&gt;
  &lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;table_schema&lt;/span&gt; &lt;span class="k"&gt;AS&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;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;table_name&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;column_name&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;col&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="n"&gt;pg_get_serial_sequence&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'%I.%I'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;table_schema&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;table_name&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;column_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;seq&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;information_schema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;columns&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;
    &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;information_schema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tables&lt;/span&gt; &lt;span class="n"&gt;tb&lt;/span&gt;
      &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;tb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;table_schema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;table_schema&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;tb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;table_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;table_name&lt;/span&gt;
    &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;table_schema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'public'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;tb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;table_type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'BASE TABLE'&lt;/span&gt;
  &lt;span class="n"&gt;LOOP&lt;/span&gt;
    &lt;span class="n"&gt;IF&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;seq&lt;/span&gt; &lt;span class="k"&gt;IS&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;THEN&lt;/span&gt;
      &lt;span class="k"&gt;EXECUTE&lt;/span&gt; &lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'SELECT setval(%L, coalesce(max(%I), 1), max(%I) IS NOT NULL) FROM %I.%I'&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;seq&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;col&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;col&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;s&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;t&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="n"&gt;RAISE&lt;/span&gt; &lt;span class="n"&gt;NOTICE&lt;/span&gt; &lt;span class="s1"&gt;'realigned % for %.%'&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;seq&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;t&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;col&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="n"&gt;LOOP&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="err"&gt;$$&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;NOTICE:  realigned public.t_ident_id_seq for t_ident.id
NOTICE:  realigned public.users_id_seq for members.id
NOTICE:  realigned public.t_always_id_seq for t_always.id
NOTICE:  realigned public.empty_t_id_seq for empty_t.id
NOTICE:  realigned public.empty2_id_seq for empty2.id
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Serial columns and identity columns come out the same way, because &lt;code&gt;pg_get_serial_sequence()&lt;/code&gt; treats them the same, and the second line of that output catches the rename trap in the act, since the sequence behind &lt;code&gt;members.id&lt;/code&gt; is still called &lt;code&gt;users_id_seq&lt;/code&gt;. Run this once at the end of a load rather than sprinkling &lt;code&gt;setval&lt;/code&gt; calls through a fixture file, where they rot every time a table is added.&lt;/p&gt;

&lt;h2&gt;
  
  
  Or stop writing explicit ids
&lt;/h2&gt;

&lt;p&gt;Every fix above is repair work on a self-inflicted wound. The ids exist in the load because a fixture file wanted user 1 to be Alice for assertions to hang off, and that convenience is what puts the sequence and the table on separate tracks in the first place, so it's a fair trade right up to the moment somebody inserts a row.&lt;/p&gt;

&lt;p&gt;However, there are two ways out. Let the database assign ids and capture them with &lt;code&gt;RETURNING&lt;/code&gt; or a CTE, so no literal id ever appears in the file, or just generate the data instead of writing it down. That is the route &lt;a href="https://seedfa.st/docs/seed-your-database" rel="noopener noreferrer"&gt;Seedfast&lt;/a&gt; takes, and rows that come out of a generator carry no hardcoded keys, so there's nothing to realign afterwards.&lt;/p&gt;

&lt;p&gt;Neither helps with the dump you restored this morning - for that, the &lt;code&gt;DO&lt;/code&gt; block above is what you want.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Why does my Postgres insert fail with a duplicate key after importing data?
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Because the import wrote explicit id values, and those never advance the sequence that supplies the column's default.&lt;/strong&gt; The sequence keeps handing out numbers from wherever it stopped, and the first one it offers is already sitting in the table. Realign it with &lt;code&gt;setval(pg_get_serial_sequence('&amp;lt;table&amp;gt;', '&amp;lt;column&amp;gt;'), (SELECT max(&amp;lt;column&amp;gt;) FROM &amp;lt;table&amp;gt;))&lt;/code&gt; and the next insert continues past your data.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I reset a Postgres sequence after loading rows?
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Use &lt;code&gt;setval()&lt;/code&gt; with the sequence resolved through &lt;code&gt;pg_get_serial_sequence()&lt;/code&gt;, not a hand-built &lt;code&gt;&amp;lt;table&amp;gt;_id_seq&lt;/code&gt; string.&lt;/strong&gt; For identity columns, &lt;code&gt;ALTER TABLE &amp;lt;table&amp;gt; ALTER COLUMN &amp;lt;column&amp;gt; RESTART WITH &amp;lt;n&amp;gt;&lt;/code&gt; does the same job in standard syntax. To move a whole schema at once, loop over &lt;code&gt;information_schema.columns&lt;/code&gt; and let the catalog name the sequences.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does TRUNCATE reset the sequence?
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Only with &lt;code&gt;RESTART IDENTITY&lt;/code&gt;.&lt;/strong&gt; A plain &lt;code&gt;TRUNCATE users&lt;/code&gt; removes every row and leaves the sequence exactly where it was, so the next insert carries on from the old high-water mark. &lt;code&gt;TRUNCATE users RESTART IDENTITY&lt;/code&gt; rewinds the sequence to its start value, which is what you usually want between test runs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is pg_get_serial_sequence valid for identity columns?
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Yes, despite the name.&lt;/strong&gt; It returns the backing sequence for &lt;code&gt;serial&lt;/code&gt;, &lt;code&gt;bigserial&lt;/code&gt;, and both flavours of &lt;code&gt;GENERATED AS IDENTITY&lt;/code&gt;, and it returns NULL for a column that has no sequence behind it, which is what makes it safe to call across every column in a schema.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related guides
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://seedfa.st/blog/postgres-seed-script" rel="noopener noreferrer"&gt;Writing a Postgres seed script that survives the next migration&lt;/a&gt;. Where the sequence reset belongs inside a maintained &lt;code&gt;seed.sql&lt;/code&gt;, alongside idempotency and insert ordering.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://seedfa.st/blog/test-data-postgresql" rel="noopener noreferrer"&gt;PostgreSQL test data: a syntax cookbook&lt;/a&gt;. The &lt;code&gt;generate_series&lt;/code&gt;, bulk copy and &lt;code&gt;pg_dump&lt;/code&gt; mechanics behind the load that gets you into this state.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://seedfa.st/blog/circular-foreign-key-seed" rel="noopener noreferrer"&gt;Circular foreign key seed: three workarounds that actually run&lt;/a&gt;. The other constraint problem that only shows up at insert time.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://seedfa.st/docs/seed-your-database" rel="noopener noreferrer"&gt;Get started with Seedfast&lt;/a&gt;. Generate connected rows against your own schema instead of maintaining a fixture file.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>postgres</category>
      <category>sql</category>
      <category>database</category>
      <category>debugging</category>
    </item>
    <item>
      <title>Compliant Test Data: Why Generated Beats Masked</title>
      <dc:creator>Mikhail Shytsko</dc:creator>
      <pubDate>Sun, 16 Aug 2026 12:29:05 +0000</pubDate>
      <link>https://dev.to/mikh-shytsko/compliant-test-data-why-generated-beats-masked-hl3</link>
      <guid>https://dev.to/mikh-shytsko/compliant-test-data-why-generated-beats-masked-hl3</guid>
      <description>&lt;p&gt;A &lt;strong&gt;compliant test data tool&lt;/strong&gt; generates rows directly from your database schema, so no production PII is ever copied or masked. Because the data was never personal to begin with, it falls outside regulations like GDPR by construction. That keeps development and staging environments out of scope, with no masking pipeline to build and no real records in lower environments to defend in an audit.&lt;/p&gt;

&lt;p&gt;If you work somewhere regulated, you already know the line "we need realistic test data" is not a lawful basis for parking real customer records in a dev database. The usual response is to mask: copy production, scrub the columns that matter, and keep the scrubbing rules in step with a schema that never stops moving. There is a less brittle option. Generate the rows from the schema and there is nothing to scrub in the first place, because no value in the dataset ever belonged to a real person — and that single shift, from copying to constructing, is what the rest of this guide unpacks under GDPR and at the point of choosing a tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;"Compliant" hinges on one question: was the data ever personal? GDPR Recital 26 puts truly anonymous, never-personal data outside scope, while pseudonymised (masked) data is still personal data and stays in scope.&lt;/li&gt;
&lt;li&gt;Generating from the schema is compliant by construction. It reads the schema and builds fresh rows, so there's no production access, no PII in the output, and no masking rules to maintain as the schema changes.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why "compliant" hinges on whether the data was ever personal
&lt;/h2&gt;

&lt;p&gt;Under GDPR, whether test data counts as "compliant" comes down to a single distinction in &lt;a href="https://gdpr-info.eu/recitals/no-26/" rel="noopener noreferrer"&gt;Recital 26&lt;/a&gt;, and the two halves of that recital draw it cleanly.&lt;/p&gt;

&lt;p&gt;Recital 26 says the principles of data protection "should therefore not apply to anonymous information, namely information which does not relate to an identified or identifiable natural person or to personal data rendered anonymous in such a manner that the data subject is not or no longer identifiable." Generated test data that never derived from a real person is the cleanest case of "does not relate to an identified or identifiable natural person." There is no data subject behind the row.&lt;/p&gt;

&lt;p&gt;Pseudonymised data is the mirror image. Masking a production export — tokenising names, scrambling emails — is pseudonymisation, and Recital 26 addresses it head-on: "Personal data which have undergone pseudonymisation, which could be attributed to a natural person by the use of additional information should be considered to be information on an identifiable natural person." Because the original stays recoverable with the right additional information, the masked copy remains personal data, and every environment holding it stays in scope.&lt;/p&gt;

&lt;p&gt;That distinction is really the &lt;a href="https://gdpr-info.eu/art-5-gdpr/" rel="noopener noreferrer"&gt;data minimization&lt;/a&gt; principle in disguise. GDPR Article 5 says personal data should be held to what is necessary, and a full production copy sitting in a lower environment is the opposite of necessary — masking shrinks the exposure, but the copy is still real data you have to govern. Generating the rows removes the personal data altogether, which is about as minimal as a dataset gets.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two approaches, side by side
&lt;/h2&gt;

&lt;p&gt;For a regulated team, the realistic choices come down to two: mask a production copy, or generate the data from the schema. Everything else is a variation on one of those, and they sit on opposite sides of the one line that actually matters here — whether a real person's data is ever present in the lower environment at all.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;Production access&lt;/th&gt;
&lt;th&gt;PII in the output&lt;/th&gt;
&lt;th&gt;In GDPR scope&lt;/th&gt;
&lt;th&gt;Pipeline to maintain&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Mask / anonymize production&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Required&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Pseudonymised (still personal)&lt;/td&gt;
&lt;td&gt;Yes (pseudonymised data is in scope)&lt;/td&gt;
&lt;td&gt;Per-column masking rules, refreshed on schema change&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Generate from schema&lt;/td&gt;
&lt;td&gt;Not required&lt;/td&gt;
&lt;td&gt;None (never personal)&lt;/td&gt;
&lt;td&gt;No (never-personal data is out of scope)&lt;/td&gt;
&lt;td&gt;None; reads the live schema each run&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Masking production data&lt;/strong&gt; is a real, established approach. Tonic Structural connects to your production database and applies masking, deterministic tokenization, and format-preserving encryption while preserving referential integrity; Delphix pairs irreversible masking with data virtualization for non-production environments, and K2View uses an entity-based model with format-preserving tokens (as of June 2026). Their shared strength is fidelity — the output inherits the real structure, distributions, and edge cases of production, because it &lt;em&gt;is&lt;/em&gt; your production data, transformed. The cost is a production connection plus the security review it triggers, a masking pipeline to update on every schema change, and output that, being pseudonymised, remains personal data under Recital 26.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Generating from the schema&lt;/strong&gt; works the other way around. The tool reads the structure — tables, columns, constraints, foreign keys — and builds valid relational rows from nothing, so no row ever began as a real person's record and there is nothing in the output anyone could re-identify. That trade suits a team whose goal is keeping dev and staging out of scope more than mirroring production exactly. Where Faker, ORM seeders, and the rest of the field fit is mapped in the &lt;a href="https://seedfa.st/blog/data-seeding-tools" rel="noopener noreferrer"&gt;data seeding tools&lt;/a&gt; comparison.&lt;/p&gt;

&lt;p&gt;Masking wins when testing depends on the &lt;em&gt;exact&lt;/em&gt; distributions and edge cases of production — fraud-model validation, or analytics that has to match prod. Generated data is realistic and correctly shaped, though it isn't your actual users' data. For development, CI, and demos, generation is the compliant default.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to look for in a compliant test data tool
&lt;/h2&gt;

&lt;p&gt;Once you've settled on generating over masking, the tool still has to deliver it. These are the criteria that decide whether it keeps you out of scope and out of maintenance:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No production access.&lt;/strong&gt; The tool should never need a connection to your production database. If it does, you're back to a pipeline that processes real data and needs a security review.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Schema-aware, foreign-key-level.&lt;/strong&gt; It should read the live schema and generate data with valid relationships across every table, rather than plausible values sitting in isolated cells. Column-level tools like Faker produce values but leave &lt;a href="https://seedfa.st/blog/referential-integrity" rel="noopener noreferrer"&gt;referential integrity&lt;/a&gt; for you to wire up by hand.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No masking rules to maintain.&lt;/strong&gt; Generation from the live schema adapts to migrations automatically. A masking config, by contrast, drifts the moment someone adds a PII column nobody flagged.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CLI/CI-native.&lt;/strong&gt; Compliant data you can only produce by hand isn't sustainable. The tool should run as a step in your pipeline so every ephemeral environment gets fresh, compliant data without a person in the loop.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Audit-defensible provenance.&lt;/strong&gt;"Our dev and staging environments do not contain customer data; we generate it from the schema" is a clean answer for an auditor. "We copy production and run a masking script" invites questions about the script's coverage.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where Seedfast fits
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://seedfa.st/" rel="noopener noreferrer"&gt;Seedfast&lt;/a&gt; is a CLI that points at a live Postgres database and generates relational data from a plain-language scope, re-reading the schema on every run. Only the metadata is ever read — table and column names, types, foreign keys, never the rows — so there's no production access to security-review and no masking step to maintain, because nothing in the pipeline was real to begin with.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;seedfast seed --scope "B2B SaaS app with 5,000 accounts, users, and 90 days of activity"


  → Connected to PostgreSQL
  → Found 28 tables, 54 foreign keys
  → Generating data...
  → Done. Seeded 41,200 rows in 9.1s

&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From there, Seedfast generates rows so that every child row points at a parent that actually exists, handling looped and self-referencing tables along the way. The values it produces are domain-shaped: plausible names, amounts in sensible ranges, distributions that don't look machine-stamped. And when next sprint's migration adds a table, the following run just picks it up, with no masking config left encoding last week's schema and quietly drifting out of date.&lt;/p&gt;

&lt;p&gt;Treat Seedfast as a technical control and nothing grander. By building data fresh, the CLI keeps production PII out of lower environments — real value, but only one line item in a program that still rests on scoping, access control, and audit logging. It is not a certification, and on its own it makes you neither GDPR nor SOC 2 compliant. If your program requires a signed BAA or a SOC 2 report from a sub-processor, check Seedfast's current status directly; don't infer it from this page, and keep treating the tool as one control among the rest. One data-path detail belongs in that same sub-processor review: to generate the data, Seedfast sends the schema definition — table and column names, types, constraints — to an AI provider, while the row values themselves never leave your database. If a column name is itself sensitive, make sure that path fits your policy. None of this is legal advice; for your own obligations, talk to qualified counsel.&lt;/p&gt;

&lt;p&gt;For where this is purely a Postgres-stack decision, the &lt;a href="https://seedfa.st/blog/best-postgres-test-data-generator" rel="noopener noreferrer"&gt;best Postgres test data generator&lt;/a&gt; comparison covers the wider tool field.&lt;/p&gt;

&lt;h2&gt;
  
  
  The regulated lenses
&lt;/h2&gt;

&lt;p&gt;The same generate-from-schema logic shows up under each framework, with a different name on the obligation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GDPR.&lt;/strong&gt; Under GDPR, never-personal data is outside scope (Recital 26), and copying production into lower environments works against data minimization (&lt;a href="https://gdpr-info.eu/art-5-gdpr/" rel="noopener noreferrer"&gt;Article 5&lt;/a&gt;). Generating from the schema satisfies minimization for non-production environments by removing the personal data entirely.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SOC 2.&lt;/strong&gt; A Type II audit asks where your test data comes from, and reducing PII in dev and staging supports the confidentiality and privacy criteria an auditor evaluates (&lt;a href="https://www.aicpa-cima.com/topic/audit-assurance/audit-and-assurance-greater-than-soc-2" rel="noopener noreferrer"&gt;AICPA SOC 2&lt;/a&gt;). "We generate it from the schema" is a cleaner control narrative than documenting a masking pipeline's coverage.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;HIPAA and PCI.&lt;/strong&gt; The same construction keeps the test-data path clear of protected health information and cardholder data; the framework-specific mechanics live in &lt;a href="https://seedfa.st/blog/hipaa-test-data" rel="noopener noreferrer"&gt;HIPAA compliant test data&lt;/a&gt; (§164.514 de-identification) and &lt;a href="https://seedfa.st/blog/test-data-for-fintech" rel="noopener noreferrer"&gt;test data for fintech&lt;/a&gt; (PCI DSS). For the staging-specific case of dropping the production copy, see &lt;a href="https://seedfa.st/blog/staging-without-prod-data" rel="noopener noreferrer"&gt;staging without production data&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Is GDPR test data the same as anonymized data?
&lt;/h3&gt;

&lt;p&gt;Anonymized and generated data get conflated, but they are different under GDPR. Anonymized data starts as real personal data and is processed to strip identifiers; under &lt;a href="https://gdpr-info.eu/recitals/no-26/" rel="noopener noreferrer"&gt;GDPR Recital 26&lt;/a&gt; it counts as only pseudonymised — and still personal data — if it can be re-identified with additional information. Data that was generated and never personal has nothing to re-identify, which is the cleaner footing for gdpr test data.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I make test data GDPR-safe?
&lt;/h3&gt;

&lt;p&gt;The most defensible route to gdpr-safe test data generation is to build the rows from your schema instead of copying and masking production. A schema-aware generator reads your tables, columns, and foreign keys, then emits realistic relational rows with no real personal data in the pipeline. Before committing to one approach, the &lt;a href="https://seedfa.st/blog/data-seeding-tools" rel="noopener noreferrer"&gt;data seeding tools&lt;/a&gt; guide weighs Faker, ORM seeders, masking, and schema-aware generation side by side — the concrete next step this page doesn't otherwise spell out.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does masking make data compliant?
&lt;/h3&gt;

&lt;p&gt;On its own, no. Masked or tokenized production data is pseudonymisation under Recital 26, so it stays personal data and its environment stays in scope, and you still carry a production connection plus the pipeline behind it. Masking earns its place on fidelity: choose it when a test genuinely depends on production's real distributions, like fraud-model validation, and choose generation for development, CI, and demos. The comparison table above shows which side of the scope line each one lands on.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is generated test data personal data under GDPR?
&lt;/h3&gt;

&lt;p&gt;Generally not, provided the generator never touches production and the output cannot be linked back to real individuals. Data built from a schema describes no actual person — there is no data subject behind the row — so it falls under Recital 26's anonymous-information exception rather than the pseudonymisation rule. The catch worth checking is provenance. If a generator quietly reads production rows to "learn" distributions, that assumption breaks.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does a compliant test data tool make my whole system compliant?
&lt;/h3&gt;

&lt;p&gt;Compliance is not something a single tool confers. A compliant test data tool removes one specific risk — production PII in non-production environments — and supports principles like data minimization. Test data compliance is one piece of a wider picture that also covers scoping, access controls, and audit logging. Generating from the schema is a strong control inside that program, and it doesn't stand in for the rest of it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related guides
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://seedfa.st/blog/data-seeding-tools" rel="noopener noreferrer"&gt;Data seeding tools&lt;/a&gt;: the regulated-team comparison of every approach, from Faker to masking to schema-aware generation.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://seedfa.st/blog/hipaa-test-data" rel="noopener noreferrer"&gt;HIPAA compliant test data&lt;/a&gt;: the healthcare-regulation lens, covering §164.514 de-identification and keeping dev and staging out of HIPAA scope.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://seedfa.st/blog/test-data-for-fintech" rel="noopener noreferrer"&gt;Test data for fintech&lt;/a&gt;: the financial-services and PCI lens on the same generate-from-schema approach.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://seedfa.st/blog/staging-without-prod-data" rel="noopener noreferrer"&gt;Staging without production data&lt;/a&gt;: the staging-specific case for generating instead of cloning production.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://seedfa.st/docs/seed-your-database" rel="noopener noreferrer"&gt;Get started&lt;/a&gt;: point &lt;a href="https://seedfa.st/" rel="noopener noreferrer"&gt;Seedfast&lt;/a&gt; at your Postgres schema and generate compliant test data in one command, no production access. See &lt;a href="https://seedfa.st/pricing" rel="noopener noreferrer"&gt;pricing&lt;/a&gt; or the &lt;a href="https://seedfa.st/#demo" rel="noopener noreferrer"&gt;one-command demo&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://seedfa.st/blog/compliant-test-data" rel="noopener noreferrer"&gt;seedfa.st&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>database</category>
      <category>devops</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Best AI Test Data Generator: Which Approach Fits Your Stack</title>
      <dc:creator>Mikhail Shytsko</dc:creator>
      <pubDate>Sun, 16 Aug 2026 12:28:52 +0000</pubDate>
      <link>https://dev.to/mikh-shytsko/best-ai-test-data-generator-which-approach-fits-your-stack-mnm</link>
      <guid>https://dev.to/mikh-shytsko/best-ai-test-data-generator-which-approach-fits-your-stack-mnm</guid>
      <description>&lt;p&gt;Every test data tool ships an AI feature now, which is exactly why searching for the best AI test data generator turns up a pile of products that share three words and little else. A faker wrapper with a chat box, a column randomizer that takes plain-English prompts, and a generator that reads your whole schema before it writes a row all answer to the same name. They behave nothing alike the moment your data has relationships to hold together.&lt;/p&gt;

&lt;p&gt;For application testing, the best AI test data generator is a schema-aware one — a tool that reads your database schema and writes rows satisfying every foreign key, where a raw LLM prompt drifts once the schema grows past a few tables and a rule-based column generator never sees the relationships at all.&lt;/p&gt;

&lt;p&gt;Three underlying approaches sit behind those product names: prompting a raw model, handing the work to a schema-aware generator, or scripting a rule-based library like Faker. Each handles a relational schema differently, and the distance between them is the whole story — a feature checklist rarely shows it. If you have already settled on the schema-aware route and want the mechanics, the &lt;a href="https://seedfa.st/blog/generate-test-data-with-ai" rel="noopener noreferrer"&gt;generate test data with AI&lt;/a&gt; playbook covers prompting an agent to seed. What follows is about choosing the approach before you choose a tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  Application test data vs model-training data
&lt;/h2&gt;

&lt;p&gt;Two different jobs hide under the same "AI synthetic data" label, and a tool built for one is rarely much good at the other. Application test data is what your software runs against, and the bar it clears is correctness: every foreign key resolving to a row that exists, unique constraints holding, the &lt;code&gt;NOT NULL&lt;/code&gt; columns filled. Realism helps you surface bugs, but if a &lt;code&gt;transaction.account_id&lt;/code&gt; points at an account nobody inserted, the app falls over before a single test tells you anything.&lt;/p&gt;

&lt;p&gt;Model-training data answers to statistical fidelity instead — the output has to mirror the distributions and edge cases of a real dataset closely enough to train on, without carrying real records across. That is what Gretel and MOSTLY AI are built for, and they do it well; it simply isn't this page's job, so anyone training or evaluating a model can stop here, though if Gretel specifically was the tool you'd settled on, &lt;a href="https://seedfa.st/compare/gretel-alternative" rel="noopener noreferrer"&gt;where Gretel users go after the NVIDIA acquisition&lt;/a&gt; is the page you actually want. A generator tuned for fidelity won't reliably drop FK-valid rows into a forty-table Postgres schema, and a tool that fills app databases isn't trying to reproduce anyone's distribution. For the methods underneath all of it — fixtures, scripts, schema-aware generation — the &lt;a href="https://seedfa.st/blog/test-data-generation" rel="noopener noreferrer"&gt;test data generation&lt;/a&gt; guide lays them out.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why an LLM alone is not an AI test data generator
&lt;/h2&gt;

&lt;p&gt;Give an LLM a single column to fill and it does the job beautifully, inventing a name, an email, a transaction amount that reads like it came straight off a real ledger. The trouble starts when those values have to agree with each other across the schema, when the &lt;code&gt;order&lt;/code&gt; it just invented has to belong to a &lt;code&gt;user&lt;/code&gt; that already exists, which belongs to an &lt;code&gt;account&lt;/code&gt;, on down a foreign-key graph the model can't take in all at once. At that point it stops being a writing task and turns into bookkeeping, and a model built to predict the next token has no special reason to keep the books straight.&lt;/p&gt;

&lt;p&gt;Underneath that is a plain context-window problem: the model only has whatever you pasted into the prompt to work from, and a forty-table schema with all its keys and constraints stops fitting in that window fast, so by the time it's generating inserts for the tables at the bottom of the dependency chain it has already lost track of what it set up at the top. The script that comes back cheerfully inserts an &lt;code&gt;order&lt;/code&gt; against a &lt;code&gt;user_id&lt;/code&gt; nothing ever created, runs clean right up until it reaches that row, then falls over with half the tables full and the other half empty. Running it again only collides with the rows the first attempt left behind, and rewording the prompt usually just moves the breakage somewhere new instead of fixing it, because none of the process is deterministic, and every pass through it costs more tokens.&lt;/p&gt;

&lt;p&gt;Neon &lt;a href="https://neon.com/blog/vibe-coding-with-ai-to-generate-synthetic-data-part-1" rel="noopener noreferrer"&gt;ran this experiment in the open&lt;/a&gt;, pointing Claude and GPT straight at the problem, and the write-up doesn't dress up the result: the models coped while the schema stayed shallow and grew less reliable as the foreign-key graph deepened and there was more structure to hold consistent than either could keep in its head.&lt;/p&gt;

&lt;p&gt;Seedfast's answer is to give the model only the part it's actually good at. The LLM, through OpenAI, reads your plain-English scope and produces the values themselves, with your schema metadata sent along so it knows the shape it's filling, while the harder job of making sure every row references something that actually exists — including tables that loop back on themselves, when the schema leaves a nullable link somewhere in the loop — stays in the tool, in ordinary deterministic code you can test. If you want the specifics of what leaves your machine and what stays on it, &lt;a href="https://seedfa.st/privacy-policy" rel="noopener noreferrer"&gt;data handling and privacy&lt;/a&gt; has them.&lt;/p&gt;

&lt;h2&gt;
  
  
  The best AI test data generator depends on the approach
&lt;/h2&gt;

&lt;p&gt;With that failure mode in view, the three approaches sort out cleanly. All of them can produce values a person would believe; they part company on everything relational — holding foreign keys valid, keeping up with a schema that changes, and running unattended in a pipeline. The table rates them on those axes rather than on how the interface feels.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Capability&lt;/th&gt;
&lt;th&gt;Raw LLM prompt&lt;/th&gt;
&lt;th&gt;Schema-aware generator (Seedfast)&lt;/th&gt;
&lt;th&gt;Rule-based / faker library&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Referential integrity&lt;/td&gt;
&lt;td&gt;Holds on a shallow schema, breaks as the graph deepens&lt;/td&gt;
&lt;td&gt;Keeps every reference valid&lt;/td&gt;
&lt;td&gt;Every foreign key wired by hand in code&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Schema freshness&lt;/td&gt;
&lt;td&gt;Knows only the schema you paste; drifts on next migration&lt;/td&gt;
&lt;td&gt;Re-reads the live schema each run, so migrations flow through&lt;/td&gt;
&lt;td&gt;Hardcoded to the schema you coded against&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CI automation&lt;/td&gt;
&lt;td&gt;Non-deterministic; a rerun collides with its own inserts&lt;/td&gt;
&lt;td&gt;One CLI or MCP step, after migrations and before the suite&lt;/td&gt;
&lt;td&gt;Runs anywhere, but the seed code is yours&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Value realism&lt;/td&gt;
&lt;td&gt;High — models write plausible names, amounts, prose&lt;/td&gt;
&lt;td&gt;High — the model writes values, the structure comes out valid&lt;/td&gt;
&lt;td&gt;Template-bound; realistic where you script it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost model&lt;/td&gt;
&lt;td&gt;Per token, and every retry is billed again&lt;/td&gt;
&lt;td&gt;Flat monthly, with no per-row or per-token meter&lt;/td&gt;
&lt;td&gt;Free and open source&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Where it earns its place&lt;/td&gt;
&lt;td&gt;One table or a throwaway prototype&lt;/td&gt;
&lt;td&gt;A live relational Postgres schema, in CI or an AI agent&lt;/td&gt;
&lt;td&gt;Deterministic fixtures and small schemas&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two rows carry most of the weight: referential integrity and schema freshness. A raw prompt and a rule-based library both hand that work back to you; only a schema-aware generator reads the live database and produces valid, connected data on its own, which is why the rest of this page treats it as the default for anything with real relationships.&lt;/p&gt;

&lt;h2&gt;
  
  
  The schema-aware approach in practice
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://seedfa.st/" rel="noopener noreferrer"&gt;Seedfast&lt;/a&gt; is the schema-aware approach packaged as a CLI and &lt;a href="https://seedfa.st/docs/mcp-setup-guide" rel="noopener noreferrer"&gt;MCP&lt;/a&gt; tool. Point it at a live PostgreSQL database, give it a plain-English scope, and it reads the schema fresh before it writes anything:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;seedfast seed --scope "100 accounts with transactions and varied balances"
  → Connected to PostgreSQL
  → Found 34 tables, 67 foreign keys
  → Generating data...
  → Done.

&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;Found 34 tables, 67 foreign keys&lt;/code&gt; line is the whole approach in miniature. Seedfast reads the schema from the database and generates rows that come out &lt;a href="https://seedfa.st/blog/referential-integrity" rel="noopener noreferrer"&gt;referentially valid&lt;/a&gt; with no manual ordering, including tables that reference each other through &lt;a href="https://seedfa.st/blog/circular-foreign-key-seed" rel="noopener noreferrer"&gt;circular foreign keys&lt;/a&gt; when the schema leaves a nullable link somewhere in the cycle — a cycle that's &lt;code&gt;NOT NULL&lt;/code&gt; on both sides with no deferral is a schema constraint no tool seeds around. It handles schemas with hundreds of tables in a single run, and because the read happens every time, a migration that adds a column or a whole table gets picked up on the next seed with nothing to edit. Called over MCP as &lt;code&gt;seedfast_run&lt;/code&gt;, an agent such as Claude Code, Cursor, or Windsurf runs the seed itself instead of scripting the inserts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; Postgres projects — Supabase, Neon, RDS, or plain Postgres — where the data has to come out relationally correct, the seed runs in CI or from an AI agent, and a predictable monthly bill beats a per-token meter.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Limitation:&lt;/strong&gt; Seedfast is Postgres-first and stays in the application-testing lane. MySQL, Oracle, and SQL Server are out of scope as first-class targets, and it builds neither ML-training sets nor masked copies of production. The free plan is permanent and takes no card, carrying $5 of credits a month; paid plans are $16 and $69, and tables and seeds stay uncapped on all three. &lt;a href="https://seedfa.st/docs/seed-your-database" rel="noopener noreferrer"&gt;Run your first seed&lt;/a&gt; takes about two minutes, or see &lt;a href="https://seedfa.st/pricing" rel="noopener noreferrer"&gt;pricing&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The schema-aware approach isn't Seedfast alone. &lt;a href="https://www.tonic.ai/products/fabricate" rel="noopener noreferrer"&gt;Tonic Fabricate&lt;/a&gt; is Tonic.ai's synthetic-data agent — distinct from Tonic Structural, their production de-identification platform — and its Live Connect feature reads a live database directly, so it produces relationally intact data without a production copy. Two things set it apart from a Postgres-only CLI. It reaches across engines, generating into and out of Postgres, MySQL, Oracle, Databricks and more with export formats Seedfast has no equivalent for; and it is built for &lt;a href="https://www.tonic.ai/solutions/use-case/reinforcement-learning" rel="noopener noreferrer"&gt;ML-training and evaluation data&lt;/a&gt; as well as software testing, so it covers the model-training job a schema-aware app-test tool deliberately leaves alone.&lt;/p&gt;

&lt;p&gt;Fabricate's pricing is credit-based and worth reading before you commit: a free tier with $5 a month in credits on a personal signup, jumping to $10 with full model access on a work signup, a Plus plan at $29 a month that includes $25 in credits, then metered turns at roughly $0.17 standard and $0.37 complex, per &lt;a href="https://www.tonic.ai/pricing" rel="noopener noreferrer"&gt;Tonic's pricing&lt;/a&gt; as of July 2026 (those rates move, so re-check). You reach it through a web agent, an API, or an SDK rather than a CLI you drop into a pipeline, so the cost of any one run is harder to forecast than a flat plan. The &lt;a href="https://seedfa.st/compare/seedfast-vs-tonic-fabricate" rel="noopener noreferrer"&gt;Seedfast vs Tonic Fabricate&lt;/a&gt; page is the full head-to-head.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rule-based approach with an AI layer
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.mockaroo.com/" rel="noopener noreferrer"&gt;Mockaroo&lt;/a&gt;'s AI field is the rule-based approach with a language model wired to the values. Rather than picking a type from the menu, you describe what you want — "retail product categories", "names of sci-fi spaceships" — and it assembles a matching list. The values get sharper; the shape does not. Rows still come out flat, one table at a time, with no foreign keys spanning them and no connection back to your live database, so a smarter value generator is sitting on a structure that was never relational. The free tier caps you at &lt;a href="https://www.mockaroo.com/plans" rel="noopener noreferrer"&gt;1,000 rows per file&lt;/a&gt; and &lt;a href="https://www.mockaroo.com/api/docs" rel="noopener noreferrer"&gt;200 API requests a day&lt;/a&gt; (as of June 2026). For a single table or a mock endpoint that is plenty; for a real schema you still export each table and reconnect the foreign keys yourself, which is the gap the &lt;a href="https://seedfa.st/compare/mockaroo-alternative" rel="noopener noreferrer"&gt;Mockaroo alternative&lt;/a&gt; comparison walks through.&lt;/p&gt;

&lt;h2&gt;
  
  
  Matching the approach to how you work
&lt;/h2&gt;

&lt;p&gt;Start from where the data has to land, not from a feature list. A single flat table or a mock API endpoint asks almost nothing of a generator; Mockaroo, Faker, or even a one-off prompt will cover it. The decision only gets interesting once the data is relational and the schema keeps changing under you.&lt;/p&gt;

&lt;p&gt;From there the approach follows your workflow. If you regenerate inside CI after every migration, or you want the agent already open in your editor to seed over MCP, a schema-aware synthetic test data tool is the only approach that runs itself and stays current with the schema — a chat window or a hand-maintained seed script can't. Cost pushes the same way: metered-per-token or per-row generation gets hard to forecast once it fires on every build, while a flat plan simply doesn't move. For the specific Postgres tools ranked one against another, the &lt;a href="https://seedfa.st/blog/best-postgres-test-data-generator" rel="noopener noreferrer"&gt;best Postgres test data generator&lt;/a&gt; comparison lays them out; for the regulated-industry angle, where copying production is off the table, the &lt;a href="https://seedfa.st/blog/data-seeding-tools" rel="noopener noreferrer"&gt;data seeding tools&lt;/a&gt; guide has it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  How is an AI test data generator different from prompting ChatGPT for data?
&lt;/h3&gt;

&lt;p&gt;The difference is where the relational bookkeeping lives. Prompt a chat model and it hands back values, then leaves you to reconcile foreign keys, insert order, and constraints across tables by hand — and it forgets your schema between sessions. A schema-aware generator reads the live database, keeps the relationships in deterministic code rather than in a context window, and produces rows where every reference stays valid. The model still writes the values; it stops being responsible for the structure.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can an AI coding agent generate test data on its own?
&lt;/h3&gt;

&lt;p&gt;It can produce plausible values, but relational data trips it up: once the foreign-key graph runs more than a few tables deep, the agent loses the insert order and leaves the database half-seeded. The pattern that holds is to hand it a schema-aware tool it can call over MCP, so it delegates the relational work instead of scripting it and then colliding with its own half-finished run on the retry. Editors like Claude Code, Cursor, and Windsurf all speak MCP, which is what makes that hand-off possible. The &lt;a href="https://seedfa.st/blog/generate-test-data-with-ai" rel="noopener noreferrer"&gt;generate test data with AI&lt;/a&gt; playbook covers the setup, and the same gap shows up one layer up the stack, in &lt;a href="https://seedfa.st/blog/agentic-qa-test-data" rel="noopener noreferrer"&gt;agentic QA tools that write and run the tests themselves&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is AI-generated synthetic test data safe for software testing?
&lt;/h3&gt;

&lt;p&gt;Yes, when it is relationally valid and generated rather than copied. Synthetic test data for software testing has to satisfy foreign keys, unique constraints, and insert order, or the app fails before a test runs — so a schema-aware generator clears the bar a flat value generator can't. Its compliance edge follows from the same design: rows invented from the schema map to no real person, so there is no production PII to mask or leak, which is why regulated teams reach for it. One caveat to weigh — schema-aware LLM tools send schema metadata, table and column names and types, to a model provider to generate values, so read a vendor's data-handling terms if the schema names themselves are sensitive. The &lt;a href="https://seedfa.st/blog/data-seeding-tools" rel="noopener noreferrer"&gt;data seeding tools&lt;/a&gt; guide covers the compliance side.&lt;/p&gt;

&lt;h2&gt;
  
  
  Seed your database from the CLI or your AI agent
&lt;/h2&gt;

&lt;p&gt;If you would rather not hand-write or babysit a seed script every time you need FK-valid application data, that is the whole reason &lt;a href="https://seedfa.st/" rel="noopener noreferrer"&gt;Seedfast&lt;/a&gt; exists. It needs no production data at all: it reads your live PostgreSQL schema and generates rows that actually connect, either as one CLI command or through the &lt;code&gt;seedfast_run&lt;/code&gt; MCP tool when you would rather an agent ran it. The free plan is enough to try the whole loop end to end, with flat monthly plans after. &lt;a href="https://seedfa.st/docs/seed-your-database" rel="noopener noreferrer"&gt;Run your first seed&lt;/a&gt; in about two minutes, or read the &lt;a href="https://seedfa.st/pricing" rel="noopener noreferrer"&gt;pricing&lt;/a&gt; first.&lt;/p&gt;

&lt;p&gt;Related guides:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://seedfa.st/blog/generate-test-data-with-ai" rel="noopener noreferrer"&gt;Generate Test Data with AI&lt;/a&gt;: the how-to playbook for prompting an AI agent to seed&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://seedfa.st/blog/synthetic-test-data-generation" rel="noopener noreferrer"&gt;Synthetic Test Data Generation&lt;/a&gt;: the process behind the tools on this page — how values get invented and what makes rows connect&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://seedfa.st/blog/test-data-generation" rel="noopener noreferrer"&gt;Test Data Generation Methods&lt;/a&gt;: the methods reference, from fixtures to schema-aware generation&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://seedfa.st/blog/data-seeding-tools" rel="noopener noreferrer"&gt;Data Seeding Tools for Regulated Teams&lt;/a&gt;: the regulated-industry compliance angle&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://seedfa.st/blog/best-ai-test-data-generator" rel="noopener noreferrer"&gt;seedfa.st&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>postgres</category>
      <category>database</category>
      <category>python</category>
    </item>
    <item>
      <title>GPT-5.6 Sol Writes the Code. It Still Can't Populate Your Database.</title>
      <dc:creator>Mikhail Shytsko</dc:creator>
      <pubDate>Sun, 16 Aug 2026 12:28:39 +0000</pubDate>
      <link>https://dev.to/mikh-shytsko/gpt-56-sol-writes-the-code-it-still-cant-populate-your-database-10mh</link>
      <guid>https://dev.to/mikh-shytsko/gpt-56-sol-writes-the-code-it-still-cant-populate-your-database-10mh</guid>
      <description>&lt;p&gt;OpenAI moved &lt;a href="https://openai.com/index/gpt-5-6/" rel="noopener noreferrer"&gt;GPT-5.6 Sol&lt;/a&gt; into general availability on July 9, 2026, and I don't intend to argue with the launch numbers. On Agents' Last Exam, Sol reaches a 54% score for roughly $760 of API spend, a level Claude Opus 4.8 never touches even after burning close to $4,000, and Sam Altman's line to &lt;a href="https://www.cnbc.com/2026/07/09/open-ai-sam-altman-chatgpt-5-6-sol.html" rel="noopener noreferrer"&gt;CNBC&lt;/a&gt; about 54% better token-efficiency on agentic coding tracks with what the chart shows. The launch page has no chart for GPT-5.6 Sol test data — the valid, connected rows all of that newly cheap code still has to run against — and that is where I expect pipelines to start slowing down.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fw7nd61ry9zp6j0acq6zl.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fw7nd61ry9zp6j0acq6zl.png" alt="Scatter chart of score versus API cost on Agents' Last Exam. GPT-5.6 Sol peaks at a 54% score for about $760 of API cost. GPT-5.5 tops out at 47%, Claude Opus 4.8 reaches 45% at nearly $4,000, Claude Fable 5 sits at 41%, and Gemini 3.1 Pro Preview at 32%." width="800" height="561"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Agents' Last Exam, score against API cost. Chart from &lt;a href="https://openai.com/index/gpt-5-6/" rel="noopener noreferrer"&gt;OpenAI's GPT-5.6 launch post&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;None of that capability transfers to the data problem, though, and the reason is structural rather than temporary. A model trained on most of the public internet has watched a thousand teams build a thousand versions of your feature, so generating the code is close to recall. Your database it has never seen. It cannot know which &lt;code&gt;customer&lt;/code&gt; rows exist in this specific instance, or what your &lt;code&gt;orders&lt;/code&gt; table checks before it accepts an insert, because nothing about your schema was ever in a training set — schemas are the one part of a codebase that genuinely resembles no other.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why GPT-5.6 Sol test data became the bottleneck
&lt;/h2&gt;

&lt;p&gt;Watch any of the launch demos all the way to the end, past the issue-reading and the file edits, and you land on the moment the model proves its work by running the test suite. Runs it against what, though? A database that already has to hold the order pointing at its customer and the shipment pointing at its warehouse, and nobody in that loop, model or human, ever inserted those rows. And because all three tiers landed in Codex and &lt;a href="https://github.blog/changelog/2026-07-09-openais-gpt-5-6-sol-terra-and-luna-are-now-available-in-github-copilot/" rel="noopener noreferrer"&gt;GitHub Copilot&lt;/a&gt; on day one, this loop is already firing inside real pull requests — against ephemeral databases that start out empty.&lt;/p&gt;

&lt;p&gt;The economics get uncomfortable once you set the two launch charts side by side. On the Artificial Analysis Intelligence Index, the only model that scores above Sol is Claude Fable 5 (59.9 against Sol's 58.9), and it spends nearly twice the money getting there, about $5,600 of measured API cost against $2,800. Capability per dollar climbs with every release; the cost of producing correct test data hasn't moved, and how would it? The binding constraint there is local knowledge of this one schema, and local knowledge does not improve when the model does.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3frax5nix1e2uhm215vn.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3frax5nix1e2uhm215vn.png" alt="Scatter chart of Artificial Analysis Intelligence Index v4.1 score versus API cost. GPT-5.6 Sol scores 58.9 at about $2,800 of API cost; Claude Fable 5 scores 59.9 at about $5,600; GPT-5.5 reaches 54.8 at about $2,650, and Claude Opus 4.8 reaches 55.7 at around $3,750." width="800" height="561"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Artificial Analysis Intelligence Index v4.1, as charted in &lt;a href="https://openai.com/index/gpt-5-6/" rel="noopener noreferrer"&gt;OpenAI's launch post&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;METR's predeployment report points at the same weakness from a different angle. Before release, the evaluator detected the highest cheating rate it has measured for any public model on its agent harness — Sol used packaging exploits to surface hidden test cases, and in one run it extracted the source file holding the expected answers, which is why METR wrote that the capability scores &lt;a href="https://metr.org/blog/2026-06-26-gpt-5-6-sol/" rel="noopener noreferrer"&gt;could not be treated as a robust measurement&lt;/a&gt;. That is not a knock on the model's coding so much as the familiar failure mode of every strong generator, output that looks correct until something external checks it. In a test database, the external check is a foreign key constraint discovering that the &lt;code&gt;customer_id&lt;/code&gt; on a generated order belongs to nobody.&lt;/p&gt;

&lt;h2&gt;
  
  
  Can't GPT-5.6 Sol just write the INSERT statements?
&lt;/h2&gt;

&lt;p&gt;For twenty rows of a flat &lt;code&gt;users&lt;/code&gt; table, GPT-5.6 Sol hands back twenty believable users, sharper than anything a Faker script produces, because inventing plausible values is squarely inside what the model does. Once the whole schema stops fitting inside the context window, which for a working Postgres database happens sooner than most people guess, the model keeps writing inserts for tables low in the dependency chain long after it has lost track of what it set up near the top. A child row lands pointing at a parent that was never created, Postgres refuses the insert, and the run stalls half-seeded (the rerun then collides with whatever the first pass already wrote). Seeding a real schema behaves more like constraint-solving than next-token prediction, since every foreign key has to resolve to a row that is genuinely there before the insert succeeds. If you are weighing options in this category, the &lt;a href="https://seedfa.st/blog/best-ai-test-data-generator" rel="noopener noreferrer"&gt;best AI test data generator&lt;/a&gt; roundup lays out where each of them gives out.&lt;/p&gt;

&lt;p&gt;The launch also moved the scaling story onto a second axis, running agents in parallel rather than only reaching for a larger single model. On Terminal-Bench 2.1, one Sol agent reaches 88.8% at roughly four minutes of simulated latency, and putting four Sol agents to work at once lifts that to 91.9% inside the same four-minute budget, where GPT-5.5 had needed seven minutes just to arrive at 85.6%. Every agent in that fleet, though, spins up its own workspace and runs against its own database, so four agents mean four schemas waiting for valid, connected rows, and not one of those parallel workers stops to insert them.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhfcjk1aj8jplebz6nvx1.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhfcjk1aj8jplebz6nvx1.png" alt="Terminal-Bench 2.1 multi-agent benchmark: a single GPT-5.6 Sol agent reaches 88.8% at about 4 minutes of latency, four Sol agents in parallel reach 91.9% at the same latency, while GPT-5.5 needs 7 minutes for 85.6%; reference lines show Claude Fable 5 at 83.1%, Claude Opus 4.8 at 78.9%, and Gemini 3.1 Pro Preview at 70.7%." width="800" height="658"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Terminal-Bench 2.1 in single- and multi-agent configurations, from &lt;a href="https://openai.com/index/gpt-5-6/" rel="noopener noreferrer"&gt;OpenAI's GPT-5.6 launch post&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually fills the database
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://seedfa.st/" rel="noopener noreferrer"&gt;Seedfast&lt;/a&gt; was built for exactly this gap. When you run it, it reads the live PostgreSQL schema and generates connected rows that satisfy the constraints, no matter how deep the schema goes. You hand it a scope in plain English and the model supplies realistic values, while the structural work it tends to botch stays inside deterministic code you can test. Getting that structure right, even where the schema loops back on itself, belongs to the seeder, whereas keeping &lt;a href="https://seedfa.st/blog/referential-integrity" rel="noopener noreferrer"&gt;referential integrity&lt;/a&gt; intact stays the database's responsibility.&lt;/p&gt;

&lt;p&gt;Exposed as an &lt;a href="https://seedfa.st/docs/mcp-setup-guide" rel="noopener noreferrer"&gt;MCP&lt;/a&gt; tool, Seedfast sits inside the same loop as the code, so the agent building your feature in &lt;a href="https://seedfa.st/blog/claude-code-mcp-database-seeding" rel="noopener noreferrer"&gt;Claude Code&lt;/a&gt;, Cursor, or another MCP client such as &lt;a href="https://seedfa.st/blog/codex-cli-database-seeding" rel="noopener noreferrer"&gt;Codex&lt;/a&gt; calls &lt;code&gt;seedfast_run&lt;/code&gt;, the branch database fills with valid data, and only then do the tests run. Run it as a CLI step after your migrations apply and every pull request meets fresh rows instead of a stale &lt;code&gt;seed.sql&lt;/code&gt; that somebody keeps hand-patching, the &lt;a href="https://seedfa.st/blog/synthetic-data-ci-cd" rel="noopener noreferrer"&gt;synthetic data for CI/CD&lt;/a&gt; pattern narrowed to a single command. When a fleet of those parallel agents spins up, each worker can call the same tool against its own branch database, and seeding keeps pace with the fleet. It re-reads the schema on every run, which lets a newly added table flow through without extra wiring, and it holds up on schemas that run to hundreds of tables. Because the free plan needs no card and the paid tiers are a flat $16 or $69 a month, the seed can fire on every push without the bill moving.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What is GPT-5.6 Sol?
&lt;/h3&gt;

&lt;p&gt;GPT-5.6 Sol is the flagship tier of OpenAI's GPT-5.6 family, released to general availability on July 9, 2026, next to the cheaper Terra and Luna tiers and pointed at the hardest coding and reasoning work. Beyond Codex and Copilot, it reached ChatGPT and the OpenAI API on the same launch day, where OpenAI &lt;a href="https://openai.com/index/gpt-5-6/" rel="noopener noreferrer"&gt;lists&lt;/a&gt; it at $5 per million input tokens and $30 per million output.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can GPT-5.6 Sol generate test data?
&lt;/h3&gt;

&lt;p&gt;On its own, GPT-5.6 Sol produces believable individual values yet cannot keep relational integrity across tables once the schema outgrows a single context window, which in practice means a handful of tables before its inserts stop agreeing with one another. Seedfast closes that gap with schema-aware, constraint-solving seeding exposed over MCP, and an agent running Sol simply delegates the relational work. The &lt;a href="https://seedfa.st/blog/generate-test-data-with-ai" rel="noopener noreferrer"&gt;generate test data with AI&lt;/a&gt; playbook walks through wiring it up.&lt;/p&gt;

&lt;h3&gt;
  
  
  What did METR find about GPT-5.6 Sol?
&lt;/h3&gt;

&lt;p&gt;METR, the independent group that evaluated GPT-5.6 Sol ahead of release, recorded a time-horizon estimate that swung from roughly 11 hours to more than 270 hours depending on whether the model's cheating runs were scored as successes (&lt;a href="https://metr.org/blog/2026-06-26-gpt-5-6-sol/" rel="noopener noreferrer"&gt;METR's predeployment evaluation&lt;/a&gt;). A spread that wide means a single headline number tells you very little about which model actually lands in your workflow, and the same caution carries straight into seeding, where correctness has to live in checks the model has no way to game.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is GPT-5.6 Sol available in GitHub Copilot?
&lt;/h3&gt;

&lt;p&gt;Yes, all three GPT-5.6 tiers arrived in GitHub Copilot on launch day, and Sol reaches the Pro+, Max, Business, and Enterprise plans under usage-based billing. That availability matters because Copilot's coding agent opens a pull request and runs the suite against an ephemeral database it never seeds for you, which puts the empty-database gap inside a tool millions of developers already keep open.&lt;/p&gt;

&lt;h2&gt;
  
  
  Seed the database GPT-5.6 Sol leaves empty
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://seedfa.st/" rel="noopener noreferrer"&gt;Seedfast&lt;/a&gt; fills the database that GPT-5.6 Sol leaves empty, running from the CLI or straight out of your AI agent to generate valid, connected rows off your live schema in a single command, with no production data required for any of it. Start on the &lt;a href="https://seedfa.st/pricing" rel="noopener noreferrer"&gt;free plan&lt;/a&gt;, and the first seed lands in roughly two minutes.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://seedfa.st/blog/gpt-5-6-sol-test-data" rel="noopener noreferrer"&gt;seedfa.st&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>postgres</category>
      <category>database</category>
      <category>mcp</category>
    </item>
    <item>
      <title>Seed Postgres From the Codex CLI With One MCP Command</title>
      <dc:creator>Mikhail Shytsko</dc:creator>
      <pubDate>Sun, 16 Aug 2026 12:28:26 +0000</pubDate>
      <link>https://dev.to/mikh-shytsko/seed-postgres-from-the-codex-cli-with-one-mcp-command-11pl</link>
      <guid>https://dev.to/mikh-shytsko/seed-postgres-from-the-codex-cli-with-one-mcp-command-11pl</guid>
      <description>&lt;p&gt;My Codex CLI session is open and the migrations have run, but the schema behind my branch is still empty. Codex CLI database seeding handles that from inside the same session, once Seedfast is registered as an MCP server the agent can call by name, and it spares me the usual fallback of hand-written inserts from the model, which tend to satisfy the column types and break on the relationships between tables — the broader &lt;a href="https://seedfa.st/blog/generate-test-data-with-ai" rel="noopener noreferrer"&gt;guide to generating test data with AI&lt;/a&gt; covers why.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; One &lt;code&gt;codex mcp add&lt;/code&gt; command registers the Seedfast MCP server in &lt;code&gt;~/.codex/config.toml&lt;/code&gt;. A plain-English prompt then calls &lt;code&gt;seedfast_run&lt;/code&gt;, which reads the foreign keys in your live Postgres schema and writes rows that stay valid, so every child row it writes points at a real parent.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Register the Seedfast MCP server in Codex CLI
&lt;/h2&gt;

&lt;p&gt;Codex CLI is OpenAI's open-source terminal coding agent, and it speaks the Model Context Protocol out of the box. One subcommand wires Seedfast into it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;codex mcp add seedfast &lt;span class="nt"&gt;--env&lt;/span&gt; &lt;span class="nv"&gt;SEEDFAST_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;sfk_live_your_api_key_here &lt;span class="nt"&gt;--&lt;/span&gt; npx &lt;span class="nt"&gt;-y&lt;/span&gt; seedfast@latest mcp

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That command writes the server into the global &lt;code&gt;~/.codex/config.toml&lt;/code&gt;, the TOML file Codex loads at startup. Open the file afterward and the new entry reads like this.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[mcp_servers.seedfast]&lt;/span&gt;
&lt;span class="py"&gt;command&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"npx"&lt;/span&gt;
&lt;span class="py"&gt;args&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"-y"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"seedfast@latest"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"mcp"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="nn"&gt;[mcp_servers.seedfast.env]&lt;/span&gt;
&lt;span class="py"&gt;SEEDFAST_API_KEY&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"sfk_live_your_api_key_here"&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run &lt;code&gt;codex mcp list&lt;/code&gt; to confirm the registration, and once you are inside a session the &lt;code&gt;/mcp&lt;/code&gt; command shows which servers are live. To check that the key authenticated, ask Codex to call &lt;code&gt;seedfast_doctor&lt;/code&gt;, which verifies the install and confirms an &lt;code&gt;sfk_live_...&lt;/code&gt; key is configured; keys come from the Seedfast dashboard. Because the server launches through &lt;code&gt;npx&lt;/code&gt;, the machine needs Node.js 18 or later, and the flags &lt;code&gt;codex mcp add&lt;/code&gt; accepts are documented in OpenAI's &lt;a href="https://developers.openai.com/codex/mcp" rel="noopener noreferrer"&gt;MCP configuration reference&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Global config versus a project-scoped .codex/config.toml
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;codex mcp add&lt;/code&gt; only ever writes the global file. To share the server through a repository, you add a project-scoped &lt;code&gt;.codex/config.toml&lt;/code&gt; by hand and check it in. Codex loads those project layers only for projects you have trusted, walking from the project root down toward your working directory, and the file closest to where you are running takes precedence on any conflicting key. A handful of security-sensitive keys are refused at project level regardless.&lt;/p&gt;

&lt;p&gt;Committing a config raises an obvious question about the API key sitting in it. Per OpenAI's config reference, Codex does not hand your whole shell environment to an MCP server, so a secret reaches the server one of two ways. You either hardcode it in the &lt;code&gt;env&lt;/code&gt; table, where it then lives in the file, or you list the variable name under &lt;code&gt;env_vars&lt;/code&gt; and let Codex forward it from its own environment at launch.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[mcp_servers.seedfast]&lt;/span&gt;
&lt;span class="py"&gt;command&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"npx"&lt;/span&gt;
&lt;span class="py"&gt;args&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"-y"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"seedfast@latest"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"mcp"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="py"&gt;env_vars&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"SEEDFAST_API_KEY"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Under &lt;code&gt;env_vars&lt;/code&gt;, the file names &lt;code&gt;SEEDFAST_API_KEY&lt;/code&gt; but never stores its value, which makes a project-scoped config safe to commit while each developer exports the key in their own shell. That forwarding is opt-in per variable, so don't assume the server can see anything else you exported; declare what it needs, one way or the other. The full set of layering and precedence rules lives in OpenAI's &lt;a href="https://developers.openai.com/codex/config-reference" rel="noopener noreferrer"&gt;config reference&lt;/a&gt;, and the equivalent setup for other clients, including the &lt;a href="https://seedfa.st/blog/claude-code-mcp-database-seeding" rel="noopener noreferrer"&gt;Claude Code version of this job&lt;/a&gt;, is in the &lt;a href="https://seedfa.st/docs/mcp-setup-guide" rel="noopener noreferrer"&gt;MCP setup guide&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a Codex CLI database seeding run looks like
&lt;/h2&gt;

&lt;p&gt;Once the server is registered, the seed itself is one message to the agent. Say a test needs a small project tree to assert against.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Use seedfast_run to fill projects, tasks and comments. A handful of projects, a realistic spread of tasks each, a few comments where they fit.

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Codex reads that and calls &lt;code&gt;seedfast_run&lt;/code&gt; with the scope, handing off the relational work. Seedfast inspects the live PostgreSQL schema at that moment and writes rows that come out valid and connected, so every task points at a real project and every comment at a real task. The same call scales with the words you choose — "a handful" keeps a unit test quick while "a few thousand tasks" gives a query planner something to strain against. Codex picked up the GPT-5.6 family on launch day, which means the model parsing your scope may well be &lt;a href="https://seedfa.st/blog/gpt-5-6-sol-test-data" rel="noopener noreferrer"&gt;GPT-5.6 Sol&lt;/a&gt;; the ordering across tables still comes from the seeder, not the model.&lt;/p&gt;

&lt;p&gt;When the run finishes, ask Codex for the summary, which reports how many tables succeeded and the total rows written, naming any table that failed; a &lt;code&gt;SELECT count(*)&lt;/code&gt; gives you the same numbers from the database side. At Seedfast's default row volume even a full schema comes back populated inside a few minutes, and a scoped seed like the one above lands while you are still reading the test that needed it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The guardrail is the connection, not the sandbox
&lt;/h2&gt;

&lt;p&gt;There is a fair worry under all of this, since you are giving an agent a write path into a database. Codex ships three sandbox modes (read-only, workspace-write, danger-full-access) with network access off until you enable it, and those govern the shell commands the agent itself runs. An MCP server sits outside that arrangement; Codex starts it as a separate, long-lived process, and in practice the sandbox does not confine it, though the docs don't spell this out. So the guardrail worth setting is the database connection you hand the server, which should belong to a dev or branch database. Codex does expose approval policies you can configure per server and per tool if you want a gate before a call runs, and because Seedfast composes every row from the schema alone, production data never enters the run.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Where does codex mcp add store the Seedfast configuration?
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;codex mcp add&lt;/code&gt; stores the Seedfast entry globally, under an &lt;code&gt;[mcp_servers.seedfast]&lt;/code&gt; table in &lt;code&gt;~/.codex/config.toml&lt;/code&gt;. Nothing project-local is created by the command; a repo-scoped &lt;code&gt;.codex/config.toml&lt;/code&gt; has to be written by hand, and &lt;code&gt;codex mcp list&lt;/code&gt; or the in-session &lt;code&gt;/mcp&lt;/code&gt; command will confirm what actually registered.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I commit the Codex MCP config without exposing my API key?
&lt;/h3&gt;

&lt;p&gt;Seedfast API keys stay out of a committed file when the variable is named under &lt;code&gt;env_vars&lt;/code&gt; instead of written into the &lt;code&gt;env&lt;/code&gt; table. OpenAI's config reference describes &lt;code&gt;env_vars&lt;/code&gt; as a pass-through list, with Codex forwarding each named variable from its own environment when the server starts, so the committed file carries only the name &lt;code&gt;SEEDFAST_API_KEY&lt;/code&gt;, never the value, and teammates supply their own keys locally. Hardcoding the key in &lt;code&gt;env&lt;/code&gt; is fine for the global file in your home directory, but it doesn't belong in anything you push.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does the Codex CLI sandbox limit what the Seedfast server can do?
&lt;/h3&gt;

&lt;p&gt;Codex CLI's sandbox is aimed at the agent's own shell commands, so in practice it does not restrict the Seedfast server, and nothing in the official docs places MCP processes inside that boundary. That leaves the choice of target database as the real limit on what a seed run can reach, with per-server and per-tool approval policies available on top when you want an explicit gate before a call.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does seedfast_run pick up schema changes between runs?
&lt;/h3&gt;

&lt;p&gt;Every &lt;code&gt;seedfast_run&lt;/code&gt; call starts with a fresh look at the current Postgres schema, so a column added by this morning's migration shows up in the next batch of rows without any config change. Scope stays up to the prompt, and generated rows keep their references valid at any size because the schema is re-read on every run.&lt;/p&gt;

&lt;h2&gt;
  
  
  Seed the schema from the terminal you already live in
&lt;/h2&gt;

&lt;p&gt;Codex CLI already sits where your migrations and tests run, and registering Seedfast means the same session can fill the schema they depend on. Keys come from the Seedfast dashboard, where the &lt;a href="https://seedfa.st/pricing" rel="noopener noreferrer"&gt;free plan&lt;/a&gt; starts without a card. Once &lt;code&gt;/mcp&lt;/code&gt; shows the server live, every empty dev schema after that is one prompt away from being populated.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://seedfa.st/blog/codex-cli-database-seeding" rel="noopener noreferrer"&gt;seedfa.st&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>mcp</category>
      <category>postgres</category>
      <category>node</category>
    </item>
    <item>
      <title>Your AI Agent Shouldn't Touch Production. Seed It a Postgres Instead.</title>
      <dc:creator>Mikhail Shytsko</dc:creator>
      <pubDate>Sun, 16 Aug 2026 12:28:12 +0000</pubDate>
      <link>https://dev.to/mikh-shytsko/your-ai-agent-shouldnt-touch-production-seed-it-a-postgres-instead-2a5c</link>
      <guid>https://dev.to/mikh-shytsko/your-ai-agent-shouldnt-touch-production-seed-it-a-postgres-instead-2a5c</guid>
      <description>&lt;p&gt;On June 4, 2026, Supabase used its Series F announcement to report that more than half of all new Supabase databases are now deployed by AI agents, with Claude Code the single largest source of that traffic. That one number turns AI agent production database access from a hypothetical some senior engineer signs off on once into a default the tooling already assumes. Two more releases the same month leaned the same direction - Vercel shipped its agent framework, Eve, in mid-June 2026, and Anthropic's Sonnet 5 arrived on June 30 with longer autonomous runs, so an agent can now stay on a multi-step database task across the kind of stretch that used to need a person watching each step. Agents provision databases, migrate them, and increasingly write to them, so the only open question is which database they get.&lt;/p&gt;

&lt;h2&gt;
  
  
  Handing an AI agent production database access, and what it costs
&lt;/h2&gt;

&lt;p&gt;The blunt answer to that question is to give the agent the same connection string a developer uses and trust it to behave. In July 2025 that trust produced the incident teams now cite by reflex, when an AI agent working in a Replit project deleted a production database during an explicit code freeze, then described what it had done only afterward - the failure wasn't exotic. An agent with write access did something destructive it was never meant to do, which is what "blast radius" names, the reach of a single unintended action. Of course, a developer holding those credentials can cause identical damage, but a person works one deliberate step at a time while an agent runs unattended, at machine speed, across more actions than anyone is reading in real time, so the same credentials carry more exposure in an agent's hands than in a developer's.&lt;/p&gt;

&lt;h2&gt;
  
  
  Isolation moves the blast radius without answering the data question
&lt;/h2&gt;

&lt;p&gt;The infrastructure vendors have a more careful answer, and it is isolation. On June 26, 2026, Neon published a branch-per-agent-session guide in which each agent run gets its own copy-on-write database branch, a forked copy the agent can wreck freely because throwing it away costs nothing and the parent sits untouched behind it. Measured against blast radius alone, this works well, since the destructive action lands on a branch nobody intends to keep. However, what it leaves untouched is the data inside that branch. A branch clones whatever its parent holds, which leaves two options and no comfortable one: fork a thin dev database and the agent runs against stale, unrepresentative rows, fork production and you have copied real customer data into a scratch environment an autonomous process is free to rummage through, the exact exposure the isolation was meant to remove. So, starting the branch empty only looks safer, and an agent handed a schema with no rows invents whatever fixtures it needs, and the tests that follow go green against nothing, which is exactly what anyone &lt;a href="https://seedfa.st/blog/vibe-coding-database" rel="noopener noreferrer"&gt;vibe coding an app and hitting the empty-database wall&lt;/a&gt; runs into the first time they open the UI instead of trusting the test output.&lt;/p&gt;

&lt;h2&gt;
  
  
  A disposable Postgres, seeded to behave like production
&lt;/h2&gt;

&lt;p&gt;The move both answers skip is to separate the two things a branch quietly fuses, the throwaway database and the data that fills it. Keep the disposable Postgres, whether that is an isolated branch or a local container that costs nothing to destroy, and change what goes inside. Rather than clone production or leave the schema bare, generate rows that fit the schema, connect correctly across foreign keys, and carry values that read like the real thing, so the agent works against a database shaped like production that never held a byte of production's actual records. That generation step is precisely where &lt;a href="https://seedfa.st/blog/generate-test-data-with-ai" rel="noopener noreferrer"&gt;raw model-written inserts fall apart&lt;/a&gt;, because a language model asked to emit SQL will happily write a foreign key pointing at a row it never created; holding the relational structure together is work for something that reads the schema rather than pattern-matching its way through it.&lt;/p&gt;

&lt;p&gt;This is the seam Seedfast is built for. It reads the live Postgres schema and writes rows that come out connected and valid, with every foreign key satisfied — including tables that reference themselves or each other in a loop. None of that path touches production, since a schema is the shape of your tables and not their contents, and every value it inserts is generated fresh. You invoke it as a CLI, &lt;code&gt;seedfast seed --scope "..."&lt;/code&gt;, or as an MCP server an agent calls on its own, and one run scales from a single table up to the several hundred a mature schema tends to carry.&lt;/p&gt;

&lt;h2&gt;
  
  
  Putting a seeded branch in front of the agent
&lt;/h2&gt;

&lt;p&gt;In practice the pattern is short to describe. Stand up a disposable Postgres, apply your migrations so the schema is current, seed it, and give the agent that connection string in place of the real one. Because Seedfast also runs as an MCP server, the seed can be the agent's own opening move, a single &lt;code&gt;seedfast_run&lt;/code&gt; call before it starts whatever work needs data underneath it, which the &lt;a href="https://seedfa.st/blog/claude-code-mcp-database-seeding" rel="noopener noreferrer"&gt;Claude Code walkthrough&lt;/a&gt; wires up from scratch and the &lt;a href="https://seedfa.st/blog/codex-cli-database-seeding" rel="noopener noreferrer"&gt;Codex CLI guide&lt;/a&gt; repeats for OpenAI's terminal agent. When the agent is &lt;a href="https://seedfa.st/blog/copilot-coding-agent-database-seeding" rel="noopener noreferrer"&gt;GitHub Copilot working in its Actions environment&lt;/a&gt;, the seed moves into its setup-steps workflow instead, and &lt;a href="https://seedfa.st/blog/antigravity-mcp-database-seeding" rel="noopener noreferrer"&gt;Antigravity's parallel agents&lt;/a&gt; read the same server from one shared config. For a loop that stays on your laptop, a &lt;a href="https://seedfa.st/blog/docker-compose-seed-database" rel="noopener noreferrer"&gt;disposable Postgres under Docker Compose&lt;/a&gt; gives you the same throwaway target with no cloud branch involved, and once the loop moves into CI, &lt;a href="https://seedfa.st/blog/synthetic-data-ci-cd" rel="noopener noreferrer"&gt;seeding on every pipeline run&lt;/a&gt; keeps each job's database current instead of restoring it from an aging copy - the setup lives in those guides. What matters here is the ordering, disposable database first, then the real schema, then generated data, and the agent last.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Why shouldn't an AI coding agent connect to your production database?
&lt;/h3&gt;

&lt;p&gt;An AI coding agent pointed at production inherits full write access and exercises it unattended, at a speed and across a step count no human review keeps pace with, which is what enlarges the blast radius of any single mistake. The widely cited Replit case from July 2025, an agent deleting a live database mid-freeze, is what that risk looks like when it lands. Handing the agent a disposable database instead retires the whole class of failure rather than trying to fence it off after the credentials are already out.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is an empty branch enough for an AI agent's test run?
&lt;/h3&gt;

&lt;p&gt;An empty branch is almost never enough, because &lt;a href="https://seedfa.st/blog/ai-agent-empty-database" rel="noopener noreferrer"&gt;an agent given a schema with no rows&lt;/a&gt; either fabricates its own fixtures or runs tests that pass without checking anything real. Copy-on-write branching, from Neon and platforms like it, solves isolation and keeps a bad run off production, yet it says nothing about whether the rows inside the branch resemble what the code meets once it ships. A branch seeded with generated, connected data is what makes the run worth trusting.&lt;/p&gt;

&lt;h3&gt;
  
  
  What did Supabase say about agents deploying databases?
&lt;/h3&gt;

&lt;p&gt;Supabase reported in its June 4, 2026 Series F announcement that AI agents now stand up more than half of all new databases on the platform, and it credited Claude Code with more of that traffic than any other tool. The figure reads as a direction more than a milestone, evidence that provisioning and writing to databases has become ordinary agent work. It sets up the practical decision that follows, namely which database an agent should ever be permitted to write against.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do you give an AI agent realistic data without copying production?
&lt;/h3&gt;

&lt;p&gt;Giving an agent realistic data without copying production comes down to generating it from the schema instead of duplicating the rows, so the database mirrors production's structure while holding none of its records. A schema-aware generator reads the table definitions and foreign keys, then writes connected rows that satisfy every constraint, which is what keeps integration tests meaningful rather than merely green. Seedfast does exactly this from a connection string, as a CLI step or an MCP call, so an agent can seed its own branch before the first test runs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which database the agent gets is the decision
&lt;/h2&gt;

&lt;p&gt;Every failure mode above, the deleted production database, the stale fork, the branch that went green because it was empty, traces back to a single choice nobody quite made on purpose, which database the agent is allowed to touch. Decide it deliberately and hand the agent a throwaway Postgres that was never production and never held a row of its data, and the safety argument mostly settles itself. Seedfast is the piece that fills that database with something worth testing against, and its &lt;a href="https://seedfa.st/pricing" rel="noopener noreferrer"&gt;free plan&lt;/a&gt; takes no card if your next agent run needs a real schema underneath it.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://seedfa.st/blog/ai-agent-production-database" rel="noopener noreferrer"&gt;seedfa.st&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>postgres</category>
      <category>devops</category>
      <category>database</category>
    </item>
    <item>
      <title>Give GitHub Copilot's Coding Agent a Seeded Postgres</title>
      <dc:creator>Mikhail Shytsko</dc:creator>
      <pubDate>Sun, 16 Aug 2026 12:27:29 +0000</pubDate>
      <link>https://dev.to/mikh-shytsko/give-github-copilots-coding-agent-a-seeded-postgres-4104</link>
      <guid>https://dev.to/mikh-shytsko/give-github-copilots-coding-agent-a-seeded-postgres-4104</guid>
      <description>&lt;p&gt;You assign an issue to Copilot, and a few minutes later a pull request is waiting for review with its checks already red. The migrations ran. What broke is everything downstream of them, the tests that expected a customer to exist, an order to join against, a row to assert on, all firing against a schema with nothing inside it. Copilot coding agent database seeding is the fix, and it comes down to one file, &lt;code&gt;.github/workflows/copilot-setup-steps.yml&lt;/code&gt;, that populates Postgres before the agent writes a line.&lt;/p&gt;

&lt;p&gt;GitHub &lt;a href="https://github.blog/changelog/2026-04-01-research-plan-and-code-with-copilot-cloud-agent/" rel="noopener noreferrer"&gt;renamed Copilot coding agent to Copilot cloud agent&lt;/a&gt; on April 1, 2026, and both names are live across its docs right now, so I'll stay with "coding agent" throughout. Why you'd hand that agent freshly generated data instead of a clone of production is &lt;a href="https://seedfa.st/blog/ai-agent-production-database" rel="noopener noreferrer"&gt;its own subject&lt;/a&gt;; here I'm taking that decision as made and wiring the environment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where a coding agent task actually runs
&lt;/h2&gt;

&lt;p&gt;Every task Copilot picks up runs in a throwaway GitHub Actions environment provisioned for that one job, on a single branch that becomes a single pull request. Nothing persists between tasks. When the run ends the environment is gone, so the database behind it has to be built from scratch each time rather than assumed from an earlier session. The setup phase you control tops out at 59 minutes, and otherwise the environment behaves like the Actions jobs you already write, down to the service-container mechanics, with one twist in the secrets model.&lt;/p&gt;

&lt;h2&gt;
  
  
  The copilot-setup-steps.yml file, and the line that breaks it silently
&lt;/h2&gt;

&lt;p&gt;The configuration lives at &lt;code&gt;.github/workflows/copilot-setup-steps.yml&lt;/code&gt;, and it reads like an ordinary workflow, except only one job inside it is read, the one named &lt;code&gt;copilot-setup-steps&lt;/code&gt;. Name it anything else and GitHub ignores the file with no error and no warning, and the agent starts against an empty database as if you'd never written it. &lt;a href="https://docs.github.com/en/copilot/how-tos/copilot-on-github/customize-copilot/customize-cloud-agent/customize-the-agent-environment" rel="noopener noreferrer"&gt;GitHub's environment customization docs&lt;/a&gt; list the honored keys, and the set is small. You get &lt;code&gt;steps&lt;/code&gt;, &lt;code&gt;permissions&lt;/code&gt;, &lt;code&gt;runs-on&lt;/code&gt;, &lt;code&gt;services&lt;/code&gt;, &lt;code&gt;snapshot&lt;/code&gt;, and &lt;code&gt;timeout-minutes&lt;/code&gt;, and anything outside that set is quietly dropped. &lt;code&gt;snapshot&lt;/code&gt; is listed as honored, but the docs don't describe what it does, so I'll leave it in the set without guessing at its behavior.&lt;/p&gt;

&lt;p&gt;The job runs before the agent starts, and it also fires on &lt;code&gt;workflow_dispatch&lt;/code&gt; and on any push or pull request touching the file itself, which lets you prove the setup is green without waiting on a real task. One requirement is easy to miss. The file has to live on your default branch to take effect at all; a copy on a feature branch does nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  A setup file that migrates and seeds Postgres
&lt;/h2&gt;

&lt;p&gt;Here's a minimal, runnable version that stands up a PostgreSQL service, runs your migrations against it, and seeds it with Seedfast.&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;Copilot setup steps&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;workflow_dispatch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;.github/workflows/copilot-setup-steps.yml&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;copilot-setup-steps&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;timeout-minutes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;20&lt;/span&gt;
    &lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;postgres&lt;/span&gt;&lt;span class="pi"&gt;:&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;postgres:16&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres&lt;/span&gt;
          &lt;span class="na"&gt;POSTGRES_DB&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;app&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="s"&gt;5432:5432&lt;/span&gt;
        &lt;span class="na"&gt;options&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;&amp;gt;-&lt;/span&gt;
          &lt;span class="s"&gt;--health-cmd "pg_isready -U postgres"&lt;/span&gt;
          &lt;span class="s"&gt;--health-interval 10s&lt;/span&gt;
          &lt;span class="s"&gt;--health-timeout 5s&lt;/span&gt;
          &lt;span class="s"&gt;--health-retries 5&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="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;Run migrations&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm run migrate&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres://postgres:postgres@localhost:5432/app&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;Seed with Seedfast&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npx seedfast seed --scope "20 customers with orders and line items" --output json&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres://postgres:postgres@localhost:5432/app&lt;/span&gt;
          &lt;span class="na"&gt;SEEDFAST_API_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.SEEDFAST_API_KEY }}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;services&lt;/code&gt; block brings up a &lt;code&gt;postgres:16&lt;/code&gt; container with a health check that holds the run until the database answers, the same pattern any Actions job uses, so nothing here is Copilot-specific until the secret. Migrations run first against &lt;code&gt;localhost:5432&lt;/code&gt;, then the seed step calls &lt;code&gt;npx seedfast seed&lt;/code&gt; (npx ships with npm) with a plain-language scope and &lt;code&gt;--output json&lt;/code&gt; for a machine-readable result. Seedfast reads the schema off that connection string and composes fresh rows whose relationships resolve, inventing every value rather than copying anything out of a real database, the generate-per-run approach behind &lt;a href="https://seedfa.st/blog/synthetic-data-ci-cd" rel="noopener noreferrer"&gt;synthetic data in CI/CD&lt;/a&gt;. Exit codes and the rest of the pipeline-side mechanics live in the &lt;a href="https://seedfa.st/docs/cicd-database-seeding" rel="noopener noreferrer"&gt;CI/CD seeding docs&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4actfktd4fy67n0pakg3.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4actfktd4fy67n0pakg3.png" alt="GitHub Actions run of the copilot-setup-steps job succeeding, with the Seedfast CLI installed and reporting ready" width="799" height="388"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you already seed this schema locally with a Compose stack, the file above is the CI cousin of that &lt;a href="https://seedfa.st/blog/docker-compose-seed-database" rel="noopener noreferrer"&gt;docker-compose seeding workflow&lt;/a&gt;, except this container is discarded the moment the task ends. One gap is worth flagging. Whether that &lt;code&gt;postgres:16&lt;/code&gt; service stays reachable at &lt;code&gt;localhost:5432&lt;/code&gt; once the agent later runs your test command itself, rather than during setup, isn't something the docs spell out, so treat the service as guaranteed for the setup steps and verify it for anything the agent runs afterward.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the agent can't see your Actions secrets
&lt;/h2&gt;

&lt;p&gt;A working setup file can still fail at the secret. The agent doesn't read your repository's Actions secrets, nor the Codespaces or Dependabot ones; it reads only the secrets you add under a separate store, at Settings → Secrets and variables → &lt;strong&gt;Copilot&lt;/strong&gt; (the tab GitHub's &lt;a href="https://docs.github.com/en/copilot/how-tos/copilot-on-github/customize-copilot/customize-cloud-agent/configure-secrets-and-variables" rel="noopener noreferrer"&gt;secrets and variables guide&lt;/a&gt; refers to as Agents). A team whose &lt;code&gt;SEEDFAST_API_KEY&lt;/code&gt; has always lived in Actions secrets watches the seed step throw an auth error that has nothing to do with the key being wrong, because from the agent's environment that value isn't there.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8irsnp0fjub7364j5dqq.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8irsnp0fjub7364j5dqq.png" alt="SEEDFAST_API_KEY configured as a Copilot secret in repository settings" width="800" height="359"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Whatever you put in that tab arrives in &lt;code&gt;copilot-setup-steps.yml&lt;/code&gt; as an environment variable, which is what the seed step expects from &lt;code&gt;${{ secrets.SEEDFAST_API_KEY }}&lt;/code&gt;. One naming detail matters on the MCP route covered next. A secret consumed by an MCP server carries the &lt;code&gt;COPILOT_MCP_&lt;/code&gt; prefix, so the same value becomes &lt;code&gt;COPILOT_MCP_SEEDFAST_API_KEY&lt;/code&gt; there, while the setup-steps file needs none.&lt;/p&gt;

&lt;h2&gt;
  
  
  The firewall that doesn't touch your setup steps
&lt;/h2&gt;

&lt;p&gt;Copilot's coding agent runs behind a firewall that's on by default, and a blocked outbound call shows up as a warning on the pull request. What matters for seeding is the firewall's scope. Per &lt;a href="https://docs.github.com/copilot/customizing-copilot/customizing-or-disabling-the-firewall-for-copilot-coding-agent" rel="noopener noreferrer"&gt;GitHub's firewall documentation&lt;/a&gt;, it "only applies to processes started by the agent via its Bash tool. It does not apply to Model Context Protocol (MCP) servers or processes started in configured Copilot setup steps." So the seed step above never meets the firewall, and you allowlist nothing for it to reach the Seedfast API.&lt;/p&gt;

&lt;p&gt;The firewall only bites when the agent itself runs the seed mid-session, calling &lt;code&gt;npx seedfast seed&lt;/code&gt; through its Bash tool. For that case, add &lt;code&gt;seedfa.st&lt;/code&gt; to the allowlist under Settings → Copilot → coding agent → Custom allowlist, and the call goes through.&lt;/p&gt;

&lt;p&gt;MCP is the other way to give the agent a seed tool it can call by name, sitting on the safe side of the firewall just as setup steps do. You configure it in the repository under Settings → Code &amp;amp; automation → Copilot → MCP servers, pasting a JSON block rather than committing a file. Local &lt;code&gt;npx&lt;/code&gt; servers are supported, using &lt;code&gt;"type": "local"&lt;/code&gt; with &lt;code&gt;"command": "npx"&lt;/code&gt;, and any secret the block references takes the &lt;code&gt;COPILOT_MCP_&lt;/code&gt; prefix. GitHub's &lt;a href="https://docs.github.com/copilot/how-tos/agents/copilot-coding-agent/extending-copilot-coding-agent-with-mcp" rel="noopener noreferrer"&gt;MCP extension guide&lt;/a&gt; carries the full schema. It's the same &lt;code&gt;npx&lt;/code&gt;-launched Seedfast server the terminal agents register, parked in settings instead of a config file, and it fits when you want the agent seeding on demand mid-task rather than once up front.&lt;/p&gt;

&lt;h2&gt;
  
  
  When this isn't worth setting up
&lt;/h2&gt;

&lt;p&gt;Not every task needs any of this. When the work never runs data-dependent code, a docs fix, a refactor nowhere near the test path, a copy change, a seeded Postgres buys you nothing and the file is dead weight on your default branch. Add it when your suite genuinely hits the database on the branches Copilot works, and skip it otherwise.&lt;/p&gt;

&lt;p&gt;There's a second cost worth naming. The Agents secrets store, the firewall allowlist, and the MCP JSON all live in repository or organization settings, so standing them up needs repo-admin, more ceremony than the terminal agents ask for. &lt;a href="https://seedfa.st/blog/claude-code-mcp-database-seeding" rel="noopener noreferrer"&gt;Claude Code&lt;/a&gt; reads one &lt;code&gt;.mcp.json&lt;/code&gt; at the repo root, &lt;a href="https://seedfa.st/blog/codex-cli-database-seeding" rel="noopener noreferrer"&gt;Codex CLI&lt;/a&gt; takes one &lt;code&gt;codex mcp add&lt;/code&gt; into a &lt;code&gt;config.toml&lt;/code&gt;, both keeping the arrangement in files a developer owns without opening GitHub settings. A team already seeding through one of those in their editor may not want a second, settings-level setup just for Copilot's cloud runs, and that's a fair call.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What must the copilot-setup-steps.yml job be called?
&lt;/h3&gt;

&lt;p&gt;The job in &lt;code&gt;copilot-setup-steps.yml&lt;/code&gt; must be named &lt;code&gt;copilot-setup-steps&lt;/code&gt; exactly, or GitHub skips the file. Nothing warns you when the name is wrong; the agent just starts without your setup having run, a common reason a database that should have been populated turns up empty. It also has to be committed to the default branch before it takes effect.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can the GitHub Copilot coding agent use a Postgres service container?
&lt;/h3&gt;

&lt;p&gt;Yes. The GitHub Copilot coding agent honors a &lt;code&gt;services:&lt;/code&gt; block in &lt;code&gt;copilot-setup-steps.yml&lt;/code&gt;, so a &lt;code&gt;postgres:16&lt;/code&gt; service container starts exactly as it would in any GitHub Actions job. Attach a health check so the setup waits until the database accepts connections, then run migrations and seeding as ordinary steps against &lt;code&gt;localhost:5432&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why doesn't my GitHub Actions secret work in copilot-setup-steps.yml?
&lt;/h3&gt;

&lt;p&gt;A GitHub Actions secret doesn't reach the coding agent because the agent reads a different store. Only secrets under the Copilot tab of Settings → Secrets and variables (the store GitHub's docs call Agents) are exposed to &lt;code&gt;copilot-setup-steps.yml&lt;/code&gt;; the Actions, Codespaces, and Dependabot stores stay invisible to it. Move the value into that tab and the reference in your YAML resolves.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does the Copilot coding agent's firewall block calls to the Seedfast API?
&lt;/h3&gt;

&lt;p&gt;Not from your setup steps. The firewall is scoped to processes the agent launches through its Bash tool, so seeding that runs inside &lt;code&gt;copilot-setup-steps.yml&lt;/code&gt; or through an MCP server reaches the Seedfast API untouched. Only when the agent runs &lt;code&gt;npx seedfast seed&lt;/code&gt; itself, mid-task, does the firewall apply, and adding &lt;code&gt;seedfa.st&lt;/code&gt; to the coding-agent allowlist clears that path.&lt;/p&gt;

&lt;h2&gt;
  
  
  Data behind the code, before review
&lt;/h2&gt;

&lt;p&gt;The payoff shows up at review time. When Copilot's pull request lands, its checks have run against generated rows with valid relationships, so red on the branch points at code the agent got wrong rather than fixtures nobody remembered to load. Seedfast keys come from the dashboard, and the &lt;a href="https://seedfa.st/pricing" rel="noopener noreferrer"&gt;free plan&lt;/a&gt; is enough to wire this into a repo and watch a task come back green off a database it built itself. Put the file in place once, and it stays a thing you set up rather than a thing you maintain, every task Copilot takes on after that starting with data already behind it.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://seedfa.st/blog/copilot-coding-agent-database-seeding" rel="noopener noreferrer"&gt;seedfa.st&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>mcp</category>
      <category>githubactions</category>
      <category>programming</category>
    </item>
    <item>
      <title>Give Google Antigravity's Agents a Seeded Postgres</title>
      <dc:creator>Mikhail Shytsko</dc:creator>
      <pubDate>Sun, 16 Aug 2026 12:27:15 +0000</pubDate>
      <link>https://dev.to/mikh-shytsko/give-google-antigravitys-agents-a-seeded-postgres-26d8</link>
      <guid>https://dev.to/mikh-shytsko/give-google-antigravitys-agents-a-seeded-postgres-26d8</guid>
      <description>&lt;p&gt;Google Antigravity's Manager Surface lets you spawn several agents at once and watch them work in parallel, one untangling a checkout bug, another chasing a flaky test, two more off on unrelated tickets. The friction shows the instant any of them reaches for data, because every agent you launched points at the same dev database and that database is empty, so the parallel runs stall together on rows nobody generated. Antigravity MCP Postgres seeding clears that before the first agent moves, letting you register Seedfast once in the config the IDE and the CLI both read, then fill the schema with one prompt while the agents are still spinning up.&lt;/p&gt;

&lt;p&gt;Antigravity ships real database tooling, yet every one of those tools reads what a schema already holds; none fills a schema holding nothing. That second job is what this page wires.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Google Antigravity is now, and what became of Gemini CLI
&lt;/h2&gt;

&lt;p&gt;Google Antigravity launched as an agentic IDE in public preview in November 2025, then widened at Google I/O on May 19, 2026, when Antigravity 2.0 arrived as an agent-first platform with a standalone Antigravity CLI beside the desktop app (&lt;a href="https://developers.googleblog.com/an-important-update-transitioning-gemini-cli-to-antigravity-cli/" rel="noopener noreferrer"&gt;Google's announcement&lt;/a&gt;, &lt;a href="https://techcrunch.com/2026/05/19/google-launches-antigravity-2-0-with-an-updated-desktop-app-and-cli-tool-at-io-2026/" rel="noopener noreferrer"&gt;TechCrunch's coverage&lt;/a&gt;). If you were leaning on Gemini CLI, that is where it went, retired on June 18, 2026 for Pro, Ultra, and free users, with only Enterprise keeping access. The move ran closer to a rename than a rewrite, the old &lt;code&gt;GEMINI_API_KEY&lt;/code&gt; becoming &lt;code&gt;AV_API_KEY&lt;/code&gt; while the workflow carried over intact (&lt;a href="https://developers.googleblog.com/an-important-update-transitioning-gemini-cli-to-antigravity-cli/" rel="noopener noreferrer"&gt;the transition post&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;What keeps the setup below short is that both surfaces read the same Model Context Protocol configuration. They load &lt;code&gt;~/.gemini/config/mcp_config.json&lt;/code&gt;, one user-level file whose &lt;code&gt;mcpServers&lt;/code&gt; block covers every surface at once, with no project-scoped MCP file to juggle the way Claude Code keeps a &lt;code&gt;.mcp.json&lt;/code&gt; per repo or Codex a &lt;code&gt;config.toml&lt;/code&gt;. Register a server once and it answers from both surfaces, so command-line seeding, once Gemini CLI's job, now runs through the Antigravity CLI. One caveat on that path, which has already moved once. Pre-2.0 guides point at &lt;code&gt;~/.gemini/antigravity/mcp_config.json&lt;/code&gt; rather than the &lt;code&gt;config/&lt;/code&gt; location the 2.0 docs use, which is why an older walkthrough may send you somewhere that looks wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  Set up Antigravity MCP Postgres in mcp_config.json
&lt;/h2&gt;

&lt;p&gt;Two things have to exist first, a Seedfast account and an API key from the dashboard, which arrives in the &lt;code&gt;sfk_live_...&lt;/code&gt; shape. Because &lt;code&gt;mcp_config.json&lt;/code&gt; is a user-level file, not something checked into a repo, the key stays in your home directory rather than traveling with a project, which sidesteps the commit-it-safely question the repo-scoped clients have to answer.&lt;/p&gt;

&lt;p&gt;Open &lt;code&gt;~/.gemini/config/mcp_config.json&lt;/code&gt; and add Seedfast under &lt;code&gt;mcpServers&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"seedfast"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"args"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"-y"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"seedfast@latest"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"mcp"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"env"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"SEEDFAST_API_KEY"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sfk_live_your_api_key_here"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The entry is the standard command/args/env shape, and Antigravity runs local stdio servers over &lt;code&gt;npx&lt;/code&gt;, so Node 18 or newer is the only prerequisite. Restart Antigravity or reload the window, and the server comes up for the IDE and the CLI together. To check the wiring, ask the agent to call &lt;code&gt;seedfast_doctor&lt;/code&gt;, which reports whether the CLI is healthy and your key authenticated. Antigravity also carries an in-app screen for adding and toggling MCP servers, and since that menu route has shifted between doc versions, treat the JSON file above as the anchor and let Google Antigravity's own MCP docs hold the current click-path.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpjgm6mvbitnclbz2xd9p.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpjgm6mvbitnclbz2xd9p.png" alt="Seedfast registered in Antigravity's mcp_config.json alongside Google's Cloud SQL server, one filling the database and one reading it" width="800" height="507"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Two more tools ride alongside &lt;code&gt;seedfast_doctor&lt;/code&gt;, &lt;code&gt;seedfast_plan&lt;/code&gt; and &lt;code&gt;seedfast_run&lt;/code&gt;, and the &lt;a href="https://seedfa.st/docs/mcp-setup-guide" rel="noopener noreferrer"&gt;MCP setup guide&lt;/a&gt; covers what each returns and how long a run takes.&lt;/p&gt;

&lt;h2&gt;
  
  
  One prompt, and the schema fills
&lt;/h2&gt;

&lt;p&gt;With the server registered, seeding reads like a request rather than a task. Say a screen you are building needs organizations that own a few users and projects. You would type something close to this into the agent panel:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Seed a handful of organizations, each with a few users and a couple of projects, using seedfast_run

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Antigravity hands that scope to &lt;code&gt;seedfast_run&lt;/code&gt;, and Seedfast takes the relational side from there, reading the schema as it stands at that moment and filling the tables so each organization exists before the users and projects that reference it, every value made up new. Scope sets scale as much as shape, so "a handful" keeps a unit test quick while "a few thousand users" gives a query planner something heavier to chew on.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5yos7bow7tiokwzta2r0.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5yos7bow7tiokwzta2r0.png" alt="Antigravity agent panel running a seed prompt with the seedfast_run summary of tables and rows" width="800" height="1070"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When it finishes, ask for the summary and the agent reports which tables were touched and how many rows landed in each, naming anything that failed; a plain &lt;code&gt;SELECT count(*)&lt;/code&gt; reads the same numbers from the database side. The same prompt works from the Antigravity CLI unchanged, since terminal and IDE resolve the same &lt;code&gt;mcp_config.json&lt;/code&gt;, so a seed first written at your desk repeats inside an SSH session or a throwaway container off that one line.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Antigravity's native database tools stop
&lt;/h2&gt;

&lt;p&gt;Google Antigravity ships database tooling of its own, worth being precise about so you know where Seedfast fits. The built-in MCP Store offers one-click installs for Google-native servers, and the AlloyDB and Cloud SQL connector is the relevant one. Once installed, it exposes &lt;code&gt;list_tables&lt;/code&gt;, &lt;code&gt;get_table_schema&lt;/code&gt;, &lt;code&gt;execute_sql&lt;/code&gt;, and &lt;code&gt;get_query_plan&lt;/code&gt; (&lt;a href="https://cloud.google.com/blog/products/data-analytics/connect-google-antigravity-ide-to-googles-data-cloud-services" rel="noopener noreferrer"&gt;per the Google Cloud blog&lt;/a&gt;), a capable kit for administering and interrogating a database from inside the agent.&lt;/p&gt;

&lt;p&gt;Read that list closely and the boundary draws itself. Every one of those tools acts on data already present, listing what exists, describing a table's shape, running a query, explaining a plan. Not one puts a row into an empty table. That is the seam this page has circled, the native tools answering what is in this schema while Seedfast answers the other half, the schema is empty, fill it with connected rows. Both sit in the same &lt;code&gt;mcp_config.json&lt;/code&gt;, the Store server for reading and querying, Seedfast for the population step the Google-native tools skip.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tool Approval, and why the target database still matters
&lt;/h2&gt;

&lt;p&gt;By default Antigravity executes MCP tool calls without pausing for approval, so a &lt;code&gt;seedfast_run&lt;/code&gt; fires the moment the agent decides to call it. Turning on Tool Approval, under Settings → Antigravity → MCP → Tool Approval, makes those calls wait for your yes, while terminal commands answer to a separate allow-list, so the two are governed independently. The exact menu labels have shifted between releases, so treat them as directional and confirm in-app.&lt;/p&gt;

&lt;p&gt;Gate the calls or not, the durable habit is the connection string, which should belong to a dev or branch database and never to production. Seedfast makes that easy to keep, since it never needs production data to work, reading only the schema and writing fresh rows, so nothing real comes within reach of the run. There is a reason beyond safety, too. Google Antigravity drew rate-limit and throttling complaints through spring 2026, so a dev database is where a stalled or retried run costs nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  When the built-in tools are enough, and when they aren't
&lt;/h2&gt;

&lt;p&gt;None of this argues for reaching past the native tools every time. When a database already carries data and you want to inspect it, run a query, or read a plan, the Store's AlloyDB and Cloud SQL server covers the job on its own, and installing a generator you won't call is clutter. The honest scope for Seedfast is the empty schema, the migration that ran against nothing, the branch cloned thin, the fresh container, the moment the tables exist and hold none of the rows a test needs. That part is what the native tools structurally cannot do.&lt;/p&gt;

&lt;p&gt;If your seat is elsewhere, the same server block moves with almost no change. Claude Code takes it in a &lt;a href="https://seedfa.st/blog/claude-code-mcp-database-seeding" rel="noopener noreferrer"&gt;project-scoped &lt;code&gt;.mcp.json&lt;/code&gt;&lt;/a&gt; and Codex CLI in a &lt;a href="https://seedfa.st/blog/codex-cli-database-seeding" rel="noopener noreferrer"&gt;&lt;code&gt;config.toml&lt;/code&gt;&lt;/a&gt;, same block, different client. The case for generating this data rather than handing an agent a &lt;a href="https://seedfa.st/blog/ai-agent-production-database" rel="noopener noreferrer"&gt;copy of production&lt;/a&gt; is its own argument, as is the reason &lt;a href="https://seedfa.st/blog/generate-test-data-with-ai" rel="noopener noreferrer"&gt;raw model-written SQL&lt;/a&gt; falls apart on the relationships between tables, both worth reading before you wire an agent to a database it can write to, alongside a wider look at &lt;a href="https://seedfa.st/blog/mcp-test-data" rel="noopener noreferrer"&gt;which MCP servers actually generate test data&lt;/a&gt; versus the ones that just move files around.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Does Google Antigravity support MCP servers like Seedfast?
&lt;/h3&gt;

&lt;p&gt;Google Antigravity supports MCP servers natively in both the IDE and the CLI, which read them from one shared &lt;code&gt;~/.gemini/config/mcp_config.json&lt;/code&gt;. Google's own servers install from the built-in catalog in a click, while a third-party server like Seedfast goes in by hand as a &lt;code&gt;command&lt;/code&gt;/&lt;code&gt;args&lt;/code&gt;/&lt;code&gt;env&lt;/code&gt; entry under &lt;code&gt;mcpServers&lt;/code&gt;, after which the agent can call its tools from either surface.&lt;/p&gt;

&lt;h3&gt;
  
  
  What happened to Gemini CLI, and does the migration change MCP config?
&lt;/h3&gt;

&lt;p&gt;Gemini CLI was retired on June 18, 2026 for Pro, Ultra, and free users and folded into the Antigravity CLI, with only the Enterprise tier keeping the old tool. The migration ran close to a rename, its main environment change being &lt;code&gt;GEMINI_API_KEY&lt;/code&gt; giving way to &lt;code&gt;AV_API_KEY&lt;/code&gt;. It leaves MCP registration alone, because the Antigravity CLI reads the same &lt;code&gt;mcp_config.json&lt;/code&gt; the IDE does, so a Seedfast entry set up for one already works for the other.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does Antigravity's AlloyDB MCP server seed a database, or only query it?
&lt;/h3&gt;

&lt;p&gt;Antigravity's AlloyDB and Cloud SQL MCP server queries and administers a database rather than seeding one. Its tools, &lt;code&gt;list_tables&lt;/code&gt;, &lt;code&gt;get_table_schema&lt;/code&gt;, &lt;code&gt;execute_sql&lt;/code&gt;, and &lt;code&gt;get_query_plan&lt;/code&gt;, all operate on data that already exists, so an empty schema stays empty however many of them the agent calls. Filling tables with connected rows is a separate job, which is why teams run a schema-aware generator like Seedfast beside the Google-native server rather than in place of it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do Antigravity MCP tool calls require approval before they run?
&lt;/h3&gt;

&lt;p&gt;Antigravity MCP tool calls run without approval by default, executing the moment the agent invokes them. Switching on Tool Approval in the MCP settings holds each call until you confirm it, while terminal commands answer to their own allow-list. For a seed in particular, the setting that earns more than the approval gate is the connection string, kept pointed at a disposable database so an unattended call has nothing valuable to reach.&lt;/p&gt;

&lt;h2&gt;
  
  
  Data in place before the agents spread out
&lt;/h2&gt;

&lt;p&gt;The parallel-agents picture from the top is really an argument for doing the data step first, and once. When the agents meet a schema that already holds organizations, users, and projects that hang together, none of them burns a run improvising fixtures or going green against nothing, and what you read back at review reflects code rather than a gap in the setup. Seedfast is the piece that puts that data there, and the &lt;a href="https://seedfa.st/pricing" rel="noopener noreferrer"&gt;free plan&lt;/a&gt; is room enough to drop in the one config block and watch a schema fill with connected rows before the agents you spawned ever notice it was empty.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://seedfa.st/blog/antigravity-mcp-database-seeding" rel="noopener noreferrer"&gt;seedfa.st&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>mcp</category>
      <category>postgres</category>
      <category>database</category>
    </item>
    <item>
      <title>Agentic QA Can Write the Tests. It Still Can't Invent the Data.</title>
      <dc:creator>Mikhail Shytsko</dc:creator>
      <pubDate>Sun, 16 Aug 2026 12:27:02 +0000</pubDate>
      <link>https://dev.to/mikh-shytsko/agentic-qa-can-write-the-tests-it-still-cant-invent-the-data-54c0</link>
      <guid>https://dev.to/mikh-shytsko/agentic-qa-can-write-the-tests-it-still-cant-invent-the-data-54c0</guid>
      <description>&lt;p&gt;An agentic QA tool reads your app, decides what's worth testing, writes the test, runs it in a real browser, and rewrites it the next morning after someone renames a button. Autonoma, QA Wolf, Momentic, and testRigor all sell a version of that loop today, and it's fair to conclude that writing and maintaining test scripts has stopped being your job. What the conclusion skips is the agentic QA test data problem sitting one layer down, in the database each of those tests runs against.&lt;/p&gt;

&lt;p&gt;None of these tools invents that database. They drive an application that already has to be full of valid, connected, believable rows, and the question of where those rows come from is one every agentic QA product still hands back to the team that bought it. The authoring of the test moved off your plate; the authoring of the data underneath it never did, and the two stay easy to confuse right up until a green suite ships a bug that only a realistic row would have caught. That lower layer has tooling of its own, a schema-aware generator like &lt;a href="https://seedfa.st/" rel="noopener noreferrer"&gt;Seedfast&lt;/a&gt;, and the reason the QA vendors leave it alone is worth a closer look.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffjb8gdkyrvrmqifjnquy.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffjb8gdkyrvrmqifjnquy.png" alt="Three-layer stack: an agentic QA layer that plans, writes, and self-heals the test is automated; beneath it, the test runs against a database that must already be full of valid, connected rows — the gap, still hand-built from seed scripts and fixtures; a schema-aware generator like Seedfast fills that layer automatically by reading the schema and generating valid, connected data." width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What agentic QA actually automated
&lt;/h2&gt;

&lt;p&gt;For a decade, end-to-end tests were written by hand and broke by hand. An engineer authored a Playwright or Cypress script, a selector shifted, the script went red, and someone lost an afternoon nursing &lt;a href="https://seedfa.st/blog/e2e-test-fixtures" rel="noopener noreferrer"&gt;brittle, hand-maintained fixtures&lt;/a&gt; back to green. Agentic QA collapses that cycle, and it's worth being specific about how far these products actually go, because they are good at the thing they set out to do.&lt;/p&gt;

&lt;p&gt;Autonoma runs the loop as a set of cooperating agents. A Planner reads the app or its codebase and generates the test cases, an Automator drives real browsers to execute them against a live preview environment, and a Maintainer notices when the UI has moved and repairs the affected tests on its own (&lt;a href="https://getautonoma.com/blog/what-an-ai-qa-agent-actually-does" rel="noopener noreferrer"&gt;getautonoma.com, May 2026&lt;/a&gt;). QA Wolf takes the managed-service route, generating and running Playwright and Appium suites so that web and native mobile flows both get covered without a team writing the automation code. testRigor lets you describe a test in plain English and compiles that description into a runnable case, which dodges brittle selectors by never asking a human to write one in the first place. Momentic sells the same core promise, an agent that authors and maintains the tests so QA engineers stop babysitting scripts; one agent-market tracker put its total funding at a reported $18.7 million, with Notion, Xero, Webflow, and Retool among its users (agentmarketcap.ai, April 8 2026).&lt;/p&gt;

&lt;p&gt;The architectures differ; the shared win doesn't. The test itself, from first intent through running assertion to self-healing upkeep, is now something a team can delegate to software, and that is a real bottleneck cleared.&lt;/p&gt;

&lt;h2&gt;
  
  
  The step every one of them still hands back to you
&lt;/h2&gt;

&lt;p&gt;Ask any of these tools where the test data comes from and the honest answers converge on the same place, which is you.&lt;/p&gt;

&lt;p&gt;Autonoma has the most developed answer in the category, and it deserves a careful description precisely because it's strong. The product ships an Environment Factory, and its own copy is candid about the mechanism. "You connect your own create and delete functions through our SDK", it reads, "so Autonoma seeds and tears down exactly like your app does — password hashing, foreign keys, business rules and all" (&lt;a href="https://getautonoma.com" rel="noopener noreferrer"&gt;getautonoma.com&lt;/a&gt;, as of July 2026). Underneath that, per its docs (&lt;a href="https://docs.autonoma.app" rel="noopener noreferrer"&gt;docs.autonoma.app&lt;/a&gt;, as of July 2026), is "one endpoint in your backend that creates isolated test data before each run and tears it down after", so every scenario starts from a clean, correct state. Because the setup runs through your application's own create logic, the rows it produces obey the same invariants production does, which is more than a generic fixture ever gives you, and it's the reason Autonoma's answer is the most credible one going even though it stops short of generating anything at all.&lt;/p&gt;

&lt;p&gt;It is also, precisely, not data generation. The Environment Factory calls a function your engineers already wrote, for every entity your tests touch, before the agent can use it — the knowledge of what a valid customer or a well-formed order looks like lives in code your team authored and maintains. Autonoma orchestrates that code well; it doesn't originate the data.&lt;/p&gt;

&lt;p&gt;The other tools sit further from generation. A competitor's April 2026 comparison characterizes QA Wolf's database setup as something that "requires coordination with QA team or manual seed scripts" (&lt;a href="https://getautonoma.com/blog/autonoma-vs-qa-wolf" rel="noopener noreferrer"&gt;getautonoma.com, April 2026&lt;/a&gt;), which is one vendor's read of a rival and worth treating as positioning. QA Wolf's own page frames a hybrid platform and service, where a team can run the tests itself or lean on QA Wolf's engineers, and neither mode claims schema-driven data generation. testRigor does ship a data feature, and its scope is the tell. The site says it "allows to easily generate unique test data based on specified format or Regex" (&lt;a href="https://testrigor.com/features/" rel="noopener noreferrer"&gt;testrigor.com/features&lt;/a&gt;, 2026). One value, one column, matched to a pattern, with no notion of the foreign key tying that column to the next table over. It's Faker's ceiling relocated inside a test-authoring product, handy for a single field and silent about the graph.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why doesn't an agentic QA tool just generate the test data itself?
&lt;/h2&gt;

&lt;p&gt;Because it's a different problem, owned by a different kind of software, and none of these vendors is in that business.&lt;/p&gt;

&lt;p&gt;A QA agent's competence is behavioral. It models what a user does and reasons about the app's visible surface, the routes and forms and states a browser can reach. Nothing in that job requires it to hold a model of how your schema's tables relate to each other or how your domain's values are really distributed, so it doesn't build one. Momentic's own December 2025 writeup of what its agents do runs from test authoring through maintenance without once mentioning the database underneath the tests (&lt;a href="https://momentic.ai/blog/ai-agents-in-qa-testing" rel="noopener noreferrer"&gt;momentic.ai, Dec 29 2025&lt;/a&gt;); in that post, at least, the data layer never comes up.&lt;/p&gt;

&lt;p&gt;Generating a realistic multi-table dataset is the opposite kind of task, and the steps don't commute: read the schema, satisfy every foreign key and check constraint, then shape the values so they read like production instead of like &lt;code&gt;random()&lt;/code&gt;. That's &lt;a href="https://seedfa.st/blog/generate-test-data-with-ai" rel="noopener noreferrer"&gt;the same constraint-solving problem a language model hits&lt;/a&gt; when you ask it to emit the inserts directly, and it's why the sharper question is often &lt;a href="https://seedfa.st/blog/ai-agent-production-database" rel="noopener noreferrer"&gt;which database an agent gets&lt;/a&gt; at all. Solving it is a product in itself, and not the product any agentic QA vendor set out to build. It's the one Seedfast did.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where a schema-aware generator fits underneath an agentic QA tool
&lt;/h2&gt;

&lt;p&gt;The fix isn't to wait for one of these vendors to grow a data engine. It's to put the missing layer directly beneath the one they already automated.&lt;/p&gt;

&lt;p&gt;That layer is a schema-aware generator, and it does the thing the QA agent structurally can't. Seedfast connects to your Postgres, reads the schema on that connection, and writes fresh values into every column so the result comes out connected and valid — nothing points at a row that isn't there, including tables that reference themselves or each other in a loop. None of what it inserts is copied from production. What Seedfast reads is schema metadata, the table and column names and constraints, never the rows inside them, though teams in regulated settings will still want to review what those names reveal. What you get is a database that behaves like production without ever having held a byte of it, standing ready before any agent, QA or coding, runs its first assertion. A run scopes down to one table or out to a schema several hundred tables wide.&lt;/p&gt;

&lt;p&gt;Stacked, the division of labor is clean. The agentic QA tool owns the question of whether the app behaves correctly; the generator owns the question of whether the database looks like production. You seed first and let the agent test second. Run Seedfast as a CLI step in the job that provisions the environment, or, because it also runs &lt;a href="https://seedfa.st/docs/mcp-setup-guide" rel="noopener noreferrer"&gt;as an MCP tool&lt;/a&gt;, let a coding agent trigger the seed itself before it hands the app off to the QA layer. A GitHub Copilot agent whose Actions environment comes up empty on every run needs the same seed wired into its setup workflow before a single test has data to run against, because &lt;a href="https://seedfa.st/blog/ai-agent-empty-database" rel="noopener noreferrer"&gt;an empty database the agent can't tell is a bug&lt;/a&gt; will pass every assertion it runs anyway. Neither tool competes with the other, and neither stands in for it. And because Seedfast is &lt;a href="https://seedfa.st/compare/test-data-tool-pricing" rel="noopener noreferrer"&gt;flat-priced rather than metered per row&lt;/a&gt;, it costs less than the flaky-suite afternoon it replaces.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What is agentic QA?
&lt;/h3&gt;

&lt;p&gt;Agentic QA is testing where an AI agent plans, writes, runs, and maintains the tests itself, reading the app or its code to decide what to cover, driving a real browser the way a person would rather than binding to fragile CSS selectors, and rewriting a test when the interface it targeted moves. The label is the vendors' own coinage, the phrase Autonoma, QA Wolf, and Momentic all reach for to name their category in 2026 marketing, which is a fair sign of how fast this became a recognized product class.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does Autonoma generate its own test data?
&lt;/h3&gt;

&lt;p&gt;No, and it doesn't claim to. Autonoma's Environment Factory calls create and delete functions your own engineers write and register through its SDK, which ships for TypeScript, Python, Elixir, Java, Ruby, Rust, Go, and PHP as of July 2026 (docs.autonoma.app). Reusing your app's real logic is why the rows come out valid, but the intelligence that knows what a valid record looks like is yours, not the agent's. It orchestrates your setup code; it doesn't invent data from your schema.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can testRigor or QA Wolf generate relational test data?
&lt;/h3&gt;

&lt;p&gt;Not in the relational sense. testRigor's data feature tops out at one value matched to a format or regex, useful for filling a field but blind to whether that field has to match a row in another table. QA Wolf describes a hybrid platform and service whose scope runs from API setup and database state management to SMS verification, native mobile execution, and multi-user workflows (&lt;a href="https://www.qawolf.com/blog/the-12-best-ai-testing-tools-in-2026" rel="noopener noreferrer"&gt;qawolf.com, 2026&lt;/a&gt;); whether a team runs the tests itself or leans on QA Wolf's engineers, nothing in that scope is schema-aware generation of relational rows.&lt;/p&gt;

&lt;h3&gt;
  
  
  How does Seedfast fit alongside an agentic QA tool?
&lt;/h3&gt;

&lt;p&gt;It runs one layer down and one step earlier. You stand up a throwaway Postgres, run your migrations, and point Seedfast at the connection string to fill it with connected rows, and only then hand that database to the QA agent to test against. Whether the agent is a browser-driving QA tool or a coding agent that seeds itself over MCP first, the sequence holds, and the QA tool never has to care where the rows came from, only that they're present and valid. The &lt;a href="https://seedfa.st/pricing" rel="noopener noreferrer"&gt;free plan&lt;/a&gt; needs no card, so evaluating the data layer doesn't require a second procurement conversation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where agentic QA test data actually comes from
&lt;/h2&gt;

&lt;p&gt;An agentic QA tool tells you whether your app behaves, but only against the rows it happens to find, and putting realistic rows there was never its job. That gap runs through every vendor in the category, which is why the durable setup stacks two automations instead of waiting for one to absorb the other. Seedfast seeds the database, the QA agent tests it, and neither pretends to do the other's work.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://seedfa.st/blog/agentic-qa-test-data" rel="noopener noreferrer"&gt;seedfa.st&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>testing</category>
      <category>ai</category>
      <category>postgres</category>
      <category>database</category>
    </item>
    <item>
      <title>Your Staging Database Is a Compliance Violation Waiting to Happen</title>
      <dc:creator>Mikhail Shytsko</dc:creator>
      <pubDate>Sun, 16 Aug 2026 12:26:49 +0000</pubDate>
      <link>https://dev.to/mikh-shytsko/your-staging-database-is-a-compliance-violation-waiting-to-happen-1bgl</link>
      <guid>https://dev.to/mikh-shytsko/your-staging-database-is-a-compliance-violation-waiting-to-happen-1bgl</guid>
      <description>&lt;p&gt;&lt;em&gt;Why production data doesn't belong in staging, and how to run staging without production data by generating it fresh from your schema.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;It's 9 AM Tuesday when your DPO stops by standup and asks, "Who has access to the staging database?"&lt;/p&gt;

&lt;p&gt;The honest answer is everyone — developers, QA, contractors, and CI all read the same three-week-old &lt;code&gt;pg_dump&lt;/code&gt; of production, with real names, emails, payment history, and, in regulated shops, &lt;a href="https://seedfa.st/blog/test-data-for-healthcare" rel="noopener noreferrer"&gt;medical records&lt;/a&gt; or financial transactions.&lt;/p&gt;

&lt;p&gt;In compliance terms, that box is a GDPR &lt;a href="https://gdpr-info.eu/art-33-gdpr/" rel="noopener noreferrer"&gt;Article 33&lt;/a&gt; breach notification in waiting, unnoticed only because nobody files "it's just staging" as a breach.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;In short&lt;/strong&gt; , production data has no place in staging. Running staging without production data means building your staging rows from the schema itself, so no masked dump is involved. Point &lt;a href="https://seedfa.st/" rel="noopener noreferrer"&gt;Seedfast&lt;/a&gt; at a database and it generates realistic, connected data at production volume without production access, so no PII reaches staging and the anonymization script goes away.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The pg_dump + Anonymize Anti-Pattern
&lt;/h2&gt;

&lt;p&gt;Here's how most teams build staging environments with &lt;a href="https://www.postgresql.org/docs/current/app-pgdump.html" rel="noopener noreferrer"&gt;&lt;code&gt;pg_dump&lt;/code&gt;&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;# Step 1: Dump production&lt;/span&gt;
pg_dump production_db &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; prod_dump.sql &lt;span class="c"&gt;# 47 GB, 3 hours&lt;/span&gt;

&lt;span class="c"&gt;# Step 2: Restore to staging&lt;/span&gt;
psql staging_db &amp;lt; prod_dump.sql &lt;span class="c"&gt;# another 2 hours&lt;/span&gt;

&lt;span class="c"&gt;# Step 3: "Anonymize" the sensitive columns&lt;/span&gt;
psql staging_db &lt;span class="nt"&gt;-f&lt;/span&gt; anonymize.sql

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And &lt;code&gt;anonymize.sql&lt;/code&gt; looks something 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;UPDATE&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'user'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="s1"&gt;'@example.com'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;phone&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'+1555000'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;LPAD&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="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'0'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;first_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Test'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;last_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'User'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;payments&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;card_last_four&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'0000'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;-- TODO: anonymize addresses (see ticket INFRA-2847, opened 8 months ago)&lt;/span&gt;
&lt;span class="c1"&gt;-- TODO: handle the new medical_records table (added last sprint)&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This pattern has four failure modes, and most teams are living at least two right now.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The Script Never Covers Everything
&lt;/h2&gt;

&lt;p&gt;That anonymization script was written six months ago, and the schema has kept moving since. Three PII-bearing tables have landed — &lt;code&gt;user_preferences&lt;/code&gt; picked up location data, and &lt;code&gt;support_tickets&lt;/code&gt; now holds free-text where customer names, account numbers, and even plaintext passwords get pasted in. Nobody has updated the script for them, and nobody will, which makes every "anonymized" refresh a quiet fiction.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Schema Drift Breaks the Restore
&lt;/h2&gt;

&lt;p&gt;Your production schema changes almost daily, so a Tuesday dump is already behind by Thursday, when a &lt;a href="https://seedfa.st/blog/migration-testing" rel="noopener noreferrer"&gt;migration&lt;/a&gt; adds a &lt;code&gt;NOT NULL&lt;/code&gt; column with no default and the restore falls over. Someone burns half a day tracing it, patching the dump by hand, and rerunning, only for the same break to return next week.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ERROR: column "verification_status" of relation "users" does not exist
-- anonymize.sql references a column that was renamed to "kyc_status" last sprint

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  3. The Dump Files Are Enormous
&lt;/h2&gt;

&lt;p&gt;A 50-million-row production database yields a dump measured in gigabytes; storing, transferring, and restoring it burns hours of infrastructure. Many teams refresh staging only weekly or monthly because anything faster is impractical.&lt;/p&gt;

&lt;p&gt;By the time that copy is a week old it has drifted from the source, missing relationships production has since formed and still carrying bugs you fixed upstream.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. You're Probably Violating GDPR Right Now
&lt;/h2&gt;

&lt;p&gt;Under GDPR, processing personal data needs a lawful basis, and "we wanted realistic staging data" is not one. The regulation demands data minimization under &lt;a href="https://gdpr-info.eu/art-5-gdpr/" rel="noopener noreferrer"&gt;Article 5&lt;/a&gt; and data protection by design under &lt;a href="https://gdpr-info.eu/art-25-gdpr/" rel="noopener noreferrer"&gt;Article 25&lt;/a&gt;, both of which a full production copy in a loosely governed environment undercuts.&lt;/p&gt;

&lt;p&gt;GDPR is not alone here, and CCPA, HIPAA, SOC 2, and PCI DSS all flag production data in non-production environments, so the copy that felt convenient is exactly what an auditor probes and a breach notice has to disclose. A &lt;a href="https://seedfa.st/blog/compliant-test-data" rel="noopener noreferrer"&gt;compliant test data tool&lt;/a&gt; keeps personal data out by construction rather than after the fact — the generate-versus-mask breakdown makes that case well beyond staging.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Alternatives (And Why They Fall Short)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Hand-Written Fixtures
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;users&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="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Alice'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'alice@test.com'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Bob'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'bob@test.com'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Charlie'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'charlie@test.com'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Fixtures carry no PII, which is their appeal, but a few hand-written rows with identical timestamps and flat distributions stress nothing and fool no one. Staging becomes a ghost town the sales team can't demo on, its dashboard showing three users named Alice, Bob, and Charlie.&lt;/p&gt;

&lt;h3&gt;
  
  
  Faker Libraries
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;faker&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Faker&lt;/span&gt;
&lt;span class="n"&gt;fake&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Faker&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="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10000&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;INSERT INTO users (name, email, created_at) VALUES (%s, %s, %s)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fake&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;name&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;fake&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;email&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;fake&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;date_time_this_year&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# Now do the same for orders... and order_items... and payments...
# And make sure the foreign keys are valid...
# And the status distributions are realistic...
# And the timestamps are chronologically consistent
&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Faker hands you random values, but the orchestration is still yours — table ordering, foreign-key resolution, realistic distributions, volume proportions. On a 40-table schema that is a multi-week build that breaks on every schema change, like the anonymization script. That hand-wired work is exactly the split the &lt;a href="https://seedfa.st/blog/best-ai-test-data-generator" rel="noopener noreferrer"&gt;best AI test data generator&lt;/a&gt; guide draws against a schema-aware tool.&lt;/p&gt;

&lt;h3&gt;
  
  
  Seedfast
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;seedfast seed &lt;span class="nt"&gt;--scope&lt;/span&gt; &lt;span class="s2"&gt;"seed 50,000 users with orders, payments, and support tickets"&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Seedfast is a CLI that reads your schema and a plain-English scope, keeps every foreign key valid, and shapes value distributions to resemble production traffic. Because every row is generated from scratch, staging holds no production PII, and no script or dump file has to track the schema. One data path is worth knowing before you adopt it. Seedfast passes your schema's shape (table and column names, types, constraints) to an AI provider to generate the data, while the row values stay in your database. If those names are themselves sensitive, clear that call with your security policy the way you would any vendor.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to run staging without production data with Seedfast
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Describe What You Need
&lt;/h3&gt;

&lt;p&gt;You &lt;a href="https://seedfa.st/docs/scoping" rel="noopener noreferrer"&gt;describe the staging environment&lt;/a&gt; you want, and Seedfast builds it from there:&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;# Full staging environment&lt;/span&gt;
seedfast seed &lt;span class="nt"&gt;--scope&lt;/span&gt; &lt;span class="s2"&gt;"seed 100,000 users with realistic profiles,
  500,000 orders spread across the last 12 months,
  payments for each order, and support tickets for 5% of orders"&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Seedfast analyzes your schema and proposes a plan:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Seeding Plan:
  public.users — 100,000 records
  public.addresses — 95,000 records
  public.orders — 500,000 records
  public.order_items — 1,400,000 records
  public.payments — 500,000 records
  public.support_tickets — 25,000 records

Total: 2,620,000 records across 6 tables

Approve? (Y/n)

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You never listed &lt;code&gt;addresses&lt;/code&gt; in your scope; Seedfast added it because &lt;code&gt;orders&lt;/code&gt; references it, then sized the line items to match, none of which you had to spell out.&lt;/p&gt;

&lt;h3&gt;
  
  
  Automate It
&lt;/h3&gt;

&lt;p&gt;For scheduled or &lt;a href="https://seedfa.st/docs/cicd-database-seeding" rel="noopener noreferrer"&gt;CI pipeline&lt;/a&gt; refreshes, set &lt;code&gt;SEEDFAST_API_KEY&lt;/code&gt; to run the CLI non-interactively:&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;# In your staging refresh script or CI pipeline&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;SEEDFAST_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt; &lt;span class="c"&gt;# from the Seedfast dashboard&lt;/span&gt;
seedfast seed &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--scope&lt;/span&gt; &lt;span class="s2"&gt;"seed 100,000 users with orders and payments"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--output&lt;/span&gt; plain

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Exporting &lt;code&gt;SEEDFAST_API_KEY&lt;/code&gt; drops the confirmation prompt, so the scope runs straight through and exits non-zero on failure. Seedfast appends to the tables in your scope without touching existing rows, so a second run stacks more data on top. For a repeatable refresh, point each run at a fresh database (an ephemeral Postgres container in CI) or truncate the target tables first:&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;# Clean slate before each scheduled refresh&lt;/span&gt;
psql &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DATABASE_URL&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;"TRUNCATE users, orders, payments RESTART IDENTITY CASCADE;"&lt;/span&gt;
seedfast seed &lt;span class="nt"&gt;--scope&lt;/span&gt; &lt;span class="s2"&gt;"seed 100,000 users with orders and payments"&lt;/span&gt; &lt;span class="nt"&gt;--output&lt;/span&gt; plain

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Scale It for Demos
&lt;/h3&gt;

&lt;p&gt;Sales demos need data that looks alive, hundreds of realistic profiles with enough activity to fill a dashboard:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;seedfast seed &lt;span class="nt"&gt;--scope&lt;/span&gt; &lt;span class="s2"&gt;"seed 5,000 users with varied subscription tiers,
  activity logs spread across the last 90 days,
  and a mix of active, churned, and trial accounts"&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's the same "must look alive, must hold no real customer data" brief &lt;a href="https://seedfa.st/blog/demo-data-generator" rel="noopener noreferrer"&gt;dedicated demo data&lt;/a&gt; has to meet.&lt;/p&gt;

&lt;h2&gt;
  
  
  Addressing the Concerns
&lt;/h2&gt;

&lt;h3&gt;
  
  
  "But will the foreign keys be valid?"
&lt;/h3&gt;

&lt;p&gt;Foreign keys stay valid because Seedfast fills every reference with a row that exists, so joins hold. When the schema loops with a nullable or deferrable side, it resolves those &lt;a href="https://seedfa.st/blog/circular-foreign-key-seed" rel="noopener noreferrer"&gt;circular dependencies&lt;/a&gt; too.&lt;/p&gt;

&lt;h3&gt;
  
  
  "What about realistic distributions?"
&lt;/h3&gt;

&lt;p&gt;This is where Seedfast pulls away from Faker. Random output is uniform; Seedfast follows &lt;a href="https://seedfa.st/docs/data-realism" rel="noopener noreferrer"&gt;realistic patterns&lt;/a&gt;, so most orders land in "completed", timestamps cluster in business hours, and amounts trace a curve that looks like real buying.&lt;/p&gt;

&lt;h3&gt;
  
  
  "Can I control the volume?"
&lt;/h3&gt;

&lt;p&gt;Volume comes from the scope, so ask for 1,000 users for a quick test and that is what you get; push it to 500,000 for a &lt;a href="https://seedfa.st/blog/load-testing-data" rel="noopener noreferrer"&gt;load test&lt;/a&gt; and Seedfast scales up, cross-table proportions intact.&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;# Quick staging refresh&lt;/span&gt;
seedfast seed &lt;span class="nt"&gt;--scope&lt;/span&gt; &lt;span class="s2"&gt;"seed 1,000 users with orders"&lt;/span&gt;

&lt;span class="c"&gt;# Load testing&lt;/span&gt;
seedfast seed &lt;span class="nt"&gt;--scope&lt;/span&gt; &lt;span class="s2"&gt;"seed 500,000 users with orders, payments, and activity logs"&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For very high row counts, see the guide to &lt;a href="https://seedfa.st/docs/large-volume-seeding" rel="noopener noreferrer"&gt;large-volume seeding&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  "Is it idempotent?"
&lt;/h3&gt;

&lt;p&gt;Not on its own — each run adds to existing rows, so seeding twice leaves two copies. For a repeatable refresh, start empty, with a throwaway database per run or the target tables truncated first.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Real Comparison
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;pg_dump + Anonymize&lt;/th&gt;
&lt;th&gt;Fixtures&lt;/th&gt;
&lt;th&gt;Faker Scripts&lt;/th&gt;
&lt;th&gt;Seedfast&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Production PII risk&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Setup time&lt;/td&gt;
&lt;td&gt;Hours&lt;/td&gt;
&lt;td&gt;Days&lt;/td&gt;
&lt;td&gt;Weeks&lt;/td&gt;
&lt;td&gt;Minutes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Schema changes&lt;/td&gt;
&lt;td&gt;Breaks scripts&lt;/td&gt;
&lt;td&gt;Breaks fixtures&lt;/td&gt;
&lt;td&gt;Breaks generators&lt;/td&gt;
&lt;td&gt;Adapts automatically&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data realism&lt;/td&gt;
&lt;td&gt;High (real data)&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;High (AI patterns)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prod data in staging&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Maintenance&lt;/td&gt;
&lt;td&gt;Ongoing&lt;/td&gt;
&lt;td&gt;Ongoing&lt;/td&gt;
&lt;td&gt;Ongoing&lt;/td&gt;
&lt;td&gt;Zero&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Tallied side by side, the trade-offs are stark. A &lt;code&gt;pg_dump&lt;/code&gt; buys realism but drags along compliance exposure, gigabytes of infrastructure, and endless maintenance, while the fixtures-or-Faker route runs the same bargain in reverse, staying safe at the cost of realism and engineering time. Seedfast sits outside that trade entirely, generating rows that are realistic and PII-free with almost no upkeep.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Is it a GDPR violation to use production data in staging?
&lt;/h3&gt;

&lt;p&gt;It is rarely illegal outright, but hard to defend. GDPR's lawful-basis and minimization duties both point away from parking a full production copy somewhere with looser access, and auditors log staging-side production data as a finding you will have to explain.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do you refresh a staging database without copying production?
&lt;/h3&gt;

&lt;p&gt;Generate it from your schema on demand, so nothing gets dumped or masked out of production. Pointed at a database connection, Seedfast writes fresh, connected rows that fit every table, so each refresh fills staging without a production record leaving home.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is synthetic data realistic enough for staging and demos?
&lt;/h3&gt;

&lt;p&gt;It is, when the generator honors your schema and models real distributions. Seedfast clusters timestamps, weights status fields toward common values, and keeps amounts in believable ranges, so a dashboard or demo reads as genuinely active.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does generated staging data keep valid foreign keys?
&lt;/h3&gt;

&lt;p&gt;Yes, every generated reference lands on a row that already exists, so joins never dangle. Schemas that loop back on themselves resolve too, provided one side of the loop is nullable or deferrable.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can staging data generation run automatically in CI?
&lt;/h3&gt;

&lt;p&gt;Yes, this is a standard unattended CI setup. With &lt;code&gt;SEEDFAST_API_KEY&lt;/code&gt; set, the CLI skips the prompt and signals success or failure through its exit code. Since each run appends, start from an empty database, fresh or truncated. The same wiring behind &lt;a href="https://seedfa.st/blog/synthetic-data-ci-cd" rel="noopener noreferrer"&gt;generating synthetic data in your CI/CD pipeline&lt;/a&gt; applies here, pointed at a staging refresh.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is synthetic staging data?
&lt;/h3&gt;

&lt;p&gt;Synthetic staging data is data built from your schema to populate a staging environment, standing in for a copied or masked production dump. Mirroring your tables and their realistic distributions while matching no real person, it carries no PII and stays clear of GDPR and SOC 2 scope. A generator like &lt;a href="https://seedfa.st/" rel="noopener noreferrer"&gt;Seedfast&lt;/a&gt; produces it straight from the live schema. For the wider tool set, see &lt;a href="https://seedfa.st/blog/data-seeding-tools" rel="noopener noreferrer"&gt;data seeding tools&lt;/a&gt;; for a Postgres comparison, &lt;a href="https://seedfa.st/blog/best-postgres-test-data-generator" rel="noopener noreferrer"&gt;the best Postgres test data generator&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting Started
&lt;/h2&gt;

&lt;p&gt;Replace your staging refresh script with a single command:&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;# Install&lt;/span&gt;
curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://seedfa.st/install | sh

&lt;span class="c"&gt;# Connect to your staging database&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"postgresql://user:pass@staging-db:5432/myapp"&lt;/span&gt;

&lt;span class="c"&gt;# Seed it&lt;/span&gt;
seedfast seed &lt;span class="nt"&gt;--scope&lt;/span&gt; &lt;span class="s2"&gt;"seed 50,000 users with orders, payments, and support tickets"&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your staging database now holds realistic data with valid relationships and no production PII. A refresh like this finishes in minutes, and when the schema shifts next sprint you rerun the same command and skip the script edits.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bring up staging without the compliance risk
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://seedfa.st/docs/seed-your-database" rel="noopener noreferrer"&gt;Get Started&lt;/a&gt; | &lt;a href="https://seedfa.st/docs" rel="noopener noreferrer"&gt;Documentation&lt;/a&gt; | &lt;a href="https://seedfa.st/pricing" rel="noopener noreferrer"&gt;Pricing&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Seedfast fills staging with production-realistic data generated from your schema, matching real volume and patterns without ever pulling a customer record out of production.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://seedfa.st/blog/staging-without-prod-data" rel="noopener noreferrer"&gt;seedfa.st&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>database</category>
      <category>devops</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Small Data, Big Lies: 6 Bugs Your Test Suite Will Never Catch</title>
      <dc:creator>Mikhail Shytsko</dc:creator>
      <pubDate>Sun, 16 Aug 2026 12:26:06 +0000</pubDate>
      <link>https://dev.to/mikh-shytsko/small-data-big-lies-6-bugs-your-test-suite-will-never-catch-52lo</link>
      <guid>https://dev.to/mikh-shytsko/small-data-big-lies-6-bugs-your-test-suite-will-never-catch-52lo</guid>
      <description>&lt;p&gt;A green test suite with 94% coverage carried this PR into production unquestioned, and two hours later the on-call phone lit up — a 47-second orders page, an export endpoint OOM-killing its pods, pagination skipping page 7. Between the suite and production the only real difference was the data, 2 million orders in one and 12 in the other. That gap is the &lt;strong&gt;data volume blind spot&lt;/strong&gt; , and almost every team has one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Six bug classes stay invisible at 10 rows and become incidents at production scale: pagination off-by-one, N+1 queries, missing indexes, memory blowups, timeout cascades, and unique constraint collisions.&lt;/li&gt;
&lt;li&gt;All six carry one signature, where passing tests hide a cost that climbs with row count until the bug surfaces in production.&lt;/li&gt;
&lt;li&gt;The fix is to run the suite against realistic volumes before you deploy, turning each production incident into a failing test.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://seedfa.st/" rel="noopener noreferrer"&gt;Seedfast&lt;/a&gt; populates your database with production-scale, foreign-key-valid rows, so your existing suite runs against realistic counts.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Volume Blind Spot
&lt;/h2&gt;

&lt;p&gt;Most test databases hold between 5 and 50 rows per table, fine for unit tests but enough to plant a dangerous assumption — that code passing at 10 rows will pass at 10 million. Small data hides whole categories of bugs that turn catastrophic at scale, and the sharpest version shows up &lt;a href="https://seedfa.st/blog/ai-agent-empty-database" rel="noopener noreferrer"&gt;when an AI agent leaves the tables empty&lt;/a&gt; rather than merely thin. Here are the six that bite most often.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Pagination Off-by-One
&lt;/h2&gt;

&lt;p&gt;This is the one everybody has shipped at least once. A paginated API looks correct while one page of 10 rows covers the whole table. Push it to 10,001 rows and page 1001 comes back empty or duplicates page 1000, depending on whether the offset math uses &lt;code&gt;&amp;gt;&lt;/code&gt; or &lt;code&gt;&amp;gt;=&lt;/code&gt;.&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="c1"&gt;-- Looks correct with small data&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="k"&gt;OFFSET&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt;

&lt;span class="c1"&gt;-- At scale: duplicate rows when created_at isn't unique&lt;/span&gt;
&lt;span class="c1"&gt;-- Rows shift between pages during concurrent inserts&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The fix is usually &lt;a href="https://use-the-index-luke.com/no-offset" rel="noopener noreferrer"&gt;cursor-based (keyset) pagination&lt;/a&gt;, but the bug hides until you have enough rows to fill several pages and timestamps dense enough to collide. The same holds in &lt;a href="https://seedfa.st/blog/load-testing-data" rel="noopener noreferrer"&gt;load testing with an empty database&lt;/a&gt;, where the plan you measure at 50 rows is nothing like production's.&lt;/p&gt;

&lt;h3&gt;
  
  
  How to catch it:
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;seedfast seed &lt;span class="nt"&gt;--scope&lt;/span&gt; &lt;span class="s2"&gt;"seed 10,000 orders with timestamps"&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then page through to the end and compare the item tally against &lt;code&gt;SELECT COUNT(*)&lt;/code&gt;; a mismatch is the bug.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. N+1 Queries
&lt;/h2&gt;

&lt;p&gt;An ORM loads a list of orders, and because each one lazily fetches its customer in a separate round-trip, the query count tracks the row count. A few orders cost a few queries nobody notices; a few thousand become a few thousand queries, and latency follows.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# 5 rows: 6 queries, 80ms
GET /api/orders → 200 OK (80ms)

# 5,000 rows: 5,001 queries, 12,400ms
GET /api/orders → 200 OK (12,400ms) # or timeout

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  How to catch it:
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;seedfast seed &lt;span class="nt"&gt;--scope&lt;/span&gt; &lt;span class="s2"&gt;"seed 5,000 orders with customers and line items"&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Enable query logging and count; any list view firing more than ~10 queries has a problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Missing Indexes
&lt;/h2&gt;

&lt;p&gt;Without an index, PostgreSQL scans 100 rows in well under a millisecond, so at development scale the query feels instant. The million-row version of that same scan runs for whole seconds, because the planner has no usable &lt;a href="https://www.postgresql.org/docs/current/indexes.html" rel="noopener noreferrer"&gt;index&lt;/a&gt; and falls back to reading every row.&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="c1"&gt;-- Fast at 100 rows (seq scan is fine)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'john@example.com'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- 1M rows: 800ms full table scan&lt;/span&gt;
&lt;span class="c1"&gt;-- With index: 0.1ms&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  How to catch it:
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;seedfast seed &lt;span class="nt"&gt;--scope&lt;/span&gt; &lt;span class="s2"&gt;"seed 100,000 users with realistic email addresses"&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then run &lt;a href="https://www.postgresql.org/docs/current/sql-explain.html" rel="noopener noreferrer"&gt;&lt;code&gt;EXPLAIN ANALYZE&lt;/code&gt;&lt;/a&gt; on your critical queries; any sequential scan on a table over 10K rows is a red flag. The &lt;a href="https://seedfa.st/blog/test-data-postgresql" rel="noopener noreferrer"&gt;PostgreSQL test data cookbook&lt;/a&gt; has the SQL patterns for generating those volumes by hand.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Memory Blowups
&lt;/h2&gt;

&lt;p&gt;Loading a whole result set into memory costs a few kilobytes at 100 rows and sails through every test, right up until the same endpoint meets 100,000 rows in production, allocates 500MB, and gets OOM-killed mid-request.&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;// Loads ALL rows into memory&lt;/span&gt;
&lt;span class="n"&gt;rows&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;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 id, email FROM users"&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;rows&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Close&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;allUsers&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Next&lt;/span&gt;&lt;span class="p"&gt;()&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="n"&gt;rows&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;Email&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;allUsers&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;allUsers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c"&gt;// grows unbounded&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same shape hides in export endpoints, report generators, batch jobs, and admin dashboards, anywhere code buffers a whole result set into an unbounded collection, harmless until production hands it enough rows to blow the heap.&lt;/p&gt;

&lt;h3&gt;
  
  
  How to catch it:
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;seedfast seed &lt;span class="nt"&gt;--scope&lt;/span&gt; &lt;span class="s2"&gt;"seed 100,000 users with profiles and activity logs"&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Hit your export and report endpoints and watch memory; RSS climbing with row count means an unbounded query buffering the whole set.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Timeout Cascades
&lt;/h2&gt;

&lt;p&gt;Service A calls Service B, which queries the database; while the tables stay small the query returns in 5ms. Once the tables have grown, the same query needs three seconds, blowing past Service A's two-second timeout, so the retry piles onto an already-busy Service B, and the circuit breaker trips and the dashboard goes red.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Small data: A → B (50ms) → DB (5ms) ✓
Large data: A → B (3.2s) → DB (2.8s) ✗ timeout
              A → B (retry) → DB ✗ timeout (DB now under double load)
              A → circuit breaker open ✗ cascade failure

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A cascade like this needs volume, roughly 500,000 rows before one query is slow enough to breach a timeout and start the chain.&lt;/p&gt;

&lt;h3&gt;
  
  
  How to catch it:
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;seedfast seed &lt;span class="nt"&gt;--scope&lt;/span&gt; &lt;span class="s2"&gt;"seed 500,000 transactions with accounts and categories"&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run your integration suite and watch for requests nearing your timeout thresholds; one at 80% today is a timeout tomorrow as the table grows.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Unique Constraint Collisions
&lt;/h2&gt;

&lt;p&gt;Test fixtures lean on hand-picked values like &lt;code&gt;user1@test.com&lt;/code&gt; and &lt;code&gt;user2@test.com&lt;/code&gt;, which never collide because a person chose each one. Real signups don't cooperate, producing duplicates at scale when two users register the same normalized address, or when a batch import slips in near-duplicates that pass validation alone but violate the constraint together.&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="c1"&gt;-- Works with 10 hand-crafted rows&lt;/span&gt;
&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'user1@test.com'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Fails at 10,000 rows with realistic data distributions&lt;/span&gt;
&lt;span class="c1"&gt;-- ERROR: duplicate key value violates unique constraint "users_email_key"&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Generated data spreads across a realistic distribution, so it includes the near-collisions a hand-written fixture set never thinks to add. Ask Seedfast for 10,000 emails and your unique constraints get exercised against genuinely varied input, all while &lt;a href="https://seedfa.st/blog/referential-integrity" rel="noopener noreferrer"&gt;foreign key relationships stay valid&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  How to catch it:
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;seedfast seed &lt;span class="nt"&gt;--scope&lt;/span&gt; &lt;span class="s2"&gt;"seed 10,000 users with realistic names and emails"&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A collision that surfaces under 10,000 realistic rows is one production never gets to spring on you, and volume closer to production scale flushes out more of these edge cases early.&lt;/p&gt;

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

&lt;p&gt;Line the six up and the same four-part fingerprint appears every time:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Invisible at small scale&lt;/strong&gt; — test suite passes, code review looks fine&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Proportional to data volume&lt;/strong&gt; — gets worse as tables grow&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Discovered in production&lt;/strong&gt; — where the data is, and the users are&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Expensive to fix after the fact&lt;/strong&gt; — incident response, hotfixes, post-mortems&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The remedy never changes either, which is to &lt;strong&gt;test against realistic data volumes before deploying.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Shift Left With One Command
&lt;/h2&gt;

&lt;p&gt;None of this needs a production copy, fixture factories hand-written for 50 tables, or SQL dumps that drift from the schema.&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;# Seed production-scale data in your dev/staging database&lt;/span&gt;
seedfast seed &lt;span class="nt"&gt;--scope&lt;/span&gt; &lt;span class="s2"&gt;"seed 100,000 users with orders, payments, and activity logs"&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Point it at your database and it works out the right proportions across related tables, holding every foreign key valid. You describe the scope in plain English, review the plan, and approve:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Seeding Plan:
  public.users — 100,000 records
  public.orders — 450,000 records
  public.payments — 320,000 records
  public.order_items — 1,200,000 records
  public.activity_logs — 2,000,000 records

Total: 4,070,000 records across 5 tables

Approve? (Y/n)

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Getting started is a free plan, no card required; if a run would outrun the credits on your &lt;a href="https://seedfa.st/pricing" rel="noopener noreferrer"&gt;plan&lt;/a&gt;, the CLI says so in the terminal. It writes only to the tables in your scope and never touches the rows already there.&lt;/p&gt;

&lt;h2&gt;
  
  
  In CI/CD
&lt;/h2&gt;

&lt;p&gt;Add a seeding step to your pipeline so the test suite runs against real data volumes on every PR:&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;Run migrations&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm run migrate&lt;/span&gt;
  &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.DATABASE_URL }}&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;Seed test database&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;seedfast seed --scope "seed 50,000 users with orders" --output plain&lt;/span&gt;
  &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;SEEDFAST_API_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.SEEDFAST_API_KEY }}&lt;/span&gt;
    &lt;span class="na"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.DATABASE_URL }}&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;Run tests&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm test&lt;/span&gt;
  &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.DATABASE_URL }}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With &lt;code&gt;SEEDFAST_API_KEY&lt;/code&gt; set, the CLI runs non-interactively, so nothing pauses for a prompt in CI. Because a run only ever adds rows and never rewrites what a table already holds, repeating the same scope keeps growing the data. A clean repeat means starting empty, from a throwaway ephemeral database or freshly truncated target tables. The &lt;a href="https://seedfa.st/docs/cicd-database-seeding" rel="noopener noreferrer"&gt;CI/CD database seeding guide&lt;/a&gt; covers the full pipeline, ephemeral per-PR databases included.&lt;/p&gt;

&lt;h3&gt;
  
  
  Start Small, Then Scale
&lt;/h3&gt;

&lt;p&gt;You don't have to jump to a million rows. Start with enough to surface the first category of bugs:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Goal&lt;/th&gt;
&lt;th&gt;Suggested scope&lt;/th&gt;
&lt;th&gt;What it catches&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Pagination bugs&lt;/td&gt;
&lt;td&gt;1,000+ rows in paginated tables&lt;/td&gt;
&lt;td&gt;Off-by-one, cursor issues&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;N+1 queries&lt;/td&gt;
&lt;td&gt;500+ rows with relationships&lt;/td&gt;
&lt;td&gt;Lazy loading performance&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Missing indexes&lt;/td&gt;
&lt;td&gt;50,000+ rows&lt;/td&gt;
&lt;td&gt;Sequential scan bottlenecks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Memory issues&lt;/td&gt;
&lt;td&gt;100,000+ rows&lt;/td&gt;
&lt;td&gt;Unbounded collection growth&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Timeout cascades&lt;/td&gt;
&lt;td&gt;500,000+ rows&lt;/td&gt;
&lt;td&gt;Cross-service timeout breaches&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;After the first bug, you'll want every PR run against realistic volumes as a habit. In our internal runs, Seedfast generated ~1M FK-valid rows on a 20-table SaaS schema in about 3.5 minutes. For the largest volumes, the &lt;a href="https://seedfa.st/docs/large-volume-seeding" rel="noopener noreferrer"&gt;large-volume seeding guide&lt;/a&gt; covers batching and scope tuning.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Why do bugs only appear with large amounts of test data?
&lt;/h3&gt;

&lt;p&gt;Because database behavior depends on how much data is in the tables, the same code takes different paths as it grows. The planner picks sequential scans on tiny tables and index scans on large ones, ORMs fan a relation into N+1 queries only once the list is long, and memory use tracks row count. At 10 rows none of that registers, so the defect waits until production supplies the volume that exposes it.&lt;/p&gt;

&lt;h3&gt;
  
  
  How much test data do I need to catch performance bugs?
&lt;/h3&gt;

&lt;p&gt;Start at roughly 10× your current test volume and work up. Each bug class trips at its own threshold, all listed in the sizing table above. Watch for the point where latency stops scaling linearly with row count; that knee usually sits right on an architectural bottleneck.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is the N+1 query problem?
&lt;/h3&gt;

&lt;p&gt;It is the pattern where fetching a list of N records fires one follow-up query per record for a related row, so a single logical read becomes N+1 trips. Each query is fast, so the endpoint looks healthy until the list grows long, which is why thin test data hides it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I just copy production data into my test database instead?
&lt;/h3&gt;

&lt;p&gt;Copying production data drags PII and compliance exposure along with it. Under HIPAA, PCI-DSS, GDPR, or SOC 2, pulling real records into dev or CI collides with data-minimization and access-control rules, making it a compliance question before an engineering one. Exports also drift from the schema and break on the next migration. Generating from the schema gives you the same volumes with no real record in the pipeline. The schema shape itself (table and column names, types, constraints) does travel to an AI provider during generation, though row values never do, so teams whose naming alone is sensitive should weigh that path.&lt;/p&gt;

&lt;h3&gt;
  
  
  How does Seedfast generate production-scale test data?
&lt;/h3&gt;

&lt;p&gt;You give Seedfast a plain-English scope and the volumes you want, and it produces connected relational rows with every foreign key valid on insert, then shows you the plan to approve first. Since it re-reads the schema on each run, a migration that would break a hand-written seed script gets picked up automatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related guides
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://seedfa.st/blog/load-testing-data" rel="noopener noreferrer"&gt;Load Testing With an Empty Database? Here's Your Problem&lt;/a&gt; — why an empty database makes every load-test number fiction&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://seedfa.st/blog/test-data-postgresql" rel="noopener noreferrer"&gt;PostgreSQL Test Data: A Syntax Cookbook&lt;/a&gt; — the raw SQL patterns for generating volumes and distributions by hand&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://seedfa.st/blog/migration-testing" rel="noopener noreferrer"&gt;ALTER TABLE, 5 Million Rows, and the Deploy That Took Down the Site&lt;/a&gt; — the migration-at-scale cousin of these volume bugs&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://seedfa.st/blog/enterprise-database-test-data" rel="noopener noreferrer"&gt;Enterprise Database Test Data: What It Actually Looks Like&lt;/a&gt; — what realistic relational test data looks like across a large schema&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://seedfa.st/docs/seed-your-database" rel="noopener noreferrer"&gt;Get started with Seedfast&lt;/a&gt; — connect your database and run your first schema-aware seed&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Test against realistic data before you ship
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://seedfa.st/docs/seed-your-database" rel="noopener noreferrer"&gt;Get Started&lt;/a&gt; | &lt;a href="https://seedfa.st/docs" rel="noopener noreferrer"&gt;Documentation&lt;/a&gt; | &lt;a href="https://seedfa.st/pricing" rel="noopener noreferrer"&gt;Pricing&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Realistic volume is the whole fix here, and Seedfast is the shortest path to a database that has it.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://seedfa.st/blog/small-data-big-lies" rel="noopener noreferrer"&gt;seedfa.st&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>testing</category>
      <category>database</category>
      <category>postgres</category>
      <category>programming</category>
    </item>
    <item>
      <title>Database Seed File Maintenance: Stop Patching seed.sql</title>
      <dc:creator>Mikhail Shytsko</dc:creator>
      <pubDate>Sun, 16 Aug 2026 12:25:53 +0000</pubDate>
      <link>https://dev.to/mikh-shytsko/database-seed-file-maintenance-stop-patching-seedsql-4oln</link>
      <guid>https://dev.to/mikh-shytsko/database-seed-file-maintenance-stop-patching-seedsql-4oln</guid>
      <description>&lt;p&gt;&lt;em&gt;Why your team quietly stopped running seed.sql months ago. A practical guide for PostgreSQL, MySQL, and ORM-based projects, not torrent &lt;code&gt;.seed&lt;/code&gt; files or BitTorrent seedboxes.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Your &lt;code&gt;seed.sql&lt;/code&gt; was committed by someone who left two years ago. It worked for three weeks. Then a migration landed, and it has been quietly broken ever since. Maybe it's called &lt;code&gt;fixtures.sql&lt;/code&gt;, &lt;code&gt;dev_data.sql&lt;/code&gt;, or &lt;code&gt;testdata/init.sql&lt;/code&gt;, but it's the same file, headed toward the same fate.&lt;/p&gt;

&lt;p&gt;This article is about &lt;strong&gt;seed file maintenance&lt;/strong&gt; and answers why static seed files drift from the schema, what that drift costs in real engineering hours, how every mainstream ORM (Rails, Prisma, Django, Laravel) hits the same wall, and what to do once you stop pretending the file works. The short answer to &lt;em&gt;how to maintain seed files&lt;/em&gt; in 2026 is that you shouldn't try. &lt;a href="https://seedfa.st/" rel="noopener noreferrer"&gt;Seedfast&lt;/a&gt; reads your live schema on every run and regenerates FK-valid data from a plain-English scope, so there is no static artifact to drift.&lt;/p&gt;

&lt;p&gt;If your team has a seed file that actually works on the current schema (without modifications, without commenting out lines, without someone saying "oh yeah, you have to run the migration first and then manually fix line 847"), you are in a vanishingly small minority, and that deserves congratulations.&lt;/p&gt;

&lt;p&gt;Everyone else is stuck with the file they all know is broken and nobody wants to fix, and this article is for them.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR. Your &lt;code&gt;seed.sql&lt;/code&gt; drifts because it's static.&lt;/strong&gt; Stop hand-editing a snapshot of last quarter's schema, and let Seedfast regenerate FK-valid data from a plain-English scope, matched against today's schema. Reference rows you assert on by literal value (admin accounts, country codes, feature flags) stay in a short hand-written &lt;code&gt;seed.sql&lt;/code&gt;. Bulk development data gets regenerated.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://seedfa.st/" rel="noopener noreferrer"&gt;Try Seedfast free →&lt;/a&gt;&lt;/strong&gt; to connect your DB and run the first seed in under five minutes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If you're here because &lt;code&gt;npx prisma db seed&lt;/code&gt; (or its Rails / Django / Laravel equivalent) just broke after a migration&lt;/strong&gt;, jump to Try it on your seed file for the install + run snippet.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Seed file maintenance is structural, not disciplinary.&lt;/strong&gt; A static file cannot keep up with a schema that changes every sprint. No amount of "be more careful" fixes that.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Every ORM has the same problem in a different syntax.&lt;/strong&gt; Rails &lt;code&gt;db/seeds.rb&lt;/code&gt;, Prisma &lt;code&gt;prisma/seed.ts&lt;/code&gt;, Django fixtures, and Laravel seeders all break for the same reason, hard-coding the shape of your data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The real cost shows up in onboarding, CI, and developer trust.&lt;/strong&gt; A broken seed file turns a 2-minute setup into a 2-hour debugging session and teaches the team to distrust all shared data tooling.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Replace the bulk-data portion of the file with Seedfast.&lt;/strong&gt; Instead of maintaining 500 lines of INSERT statements, describe the data you want in plain English; Seedfast regenerates matching rows straight from the current schema. Keep a small hand-written &lt;code&gt;seed.sql&lt;/code&gt; for production reference rows and named-by-ID test fixtures.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Life and Death of seed.sql
&lt;/h2&gt;

&lt;p&gt;The lifecycle is so predictable it could be a template.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Week 1: Creation.&lt;/strong&gt; A motivated developer, usually someone onboarding, writes a seed file that inserts users, orders, and products, whatever the app needs to look populated. It works, the PR gets merged, and the team is grateful. Local development is smooth enough that people actually use the app locally instead of staring at empty states.&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="c1"&gt;-- seed.sql (v1, the golden age)&lt;/span&gt;
&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;users&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="n"&gt;name&lt;/span&gt;&lt;span class="p"&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;created_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;VALUES&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Alice Johnson'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'alice@example.com'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'2025-01-15'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Bob Smith'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'bob@example.com'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'2025-02-20'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Carol Davis'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'carol@example.com'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'2025-03-10'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;orders&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="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;VALUES&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;99&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;99&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'completed'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'2025-01-20'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;149&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'completed'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'2025-02-15'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;75&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'pending'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'2025-03-01'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&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;id&lt;/span&gt;&lt;span class="p"&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;price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;category&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;VALUES&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Widget Pro'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;49&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;99&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'electronics'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Gadget Plus'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;29&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;99&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'electronics'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Thingamajig'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;19&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;99&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'accessories'&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;Week 3: First crack.&lt;/strong&gt; A migration adds a &lt;code&gt;NOT NULL&lt;/code&gt; column to &lt;code&gt;users&lt;/code&gt;, but the seed file doesn't include it. New developers run the seed, get an error, and ask in Slack. Someone replies "oh just add &lt;code&gt;role DEFAULT 'user'&lt;/code&gt; to the users table insert," but nobody updates the file.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ERROR: null value in column "role" of relation "users" violates not-null constraint
DETAIL: Failing row contains (1, Alice Johnson, alice@example.com, 2025-01-15, null)

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Month 2: The patch.&lt;/strong&gt; Someone gets frustrated enough to fix it, adding the missing column and noticing along the way that &lt;code&gt;orders&lt;/code&gt; now has a &lt;code&gt;shipping_address_id&lt;/code&gt; foreign key to a new &lt;code&gt;addresses&lt;/code&gt; table, so they add an &lt;code&gt;addresses&lt;/code&gt; insert block too. The PR is 200 lines of SQL changes for a file that was supposed to be "set and forget," and it passes review because nobody wants to think about it too hard.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Month 4: The second break.&lt;/strong&gt; The &lt;code&gt;products&lt;/code&gt; table gets renamed to &lt;code&gt;catalog_items&lt;/code&gt; as part of a domain modeling cleanup, but the seed file still references &lt;code&gt;products&lt;/code&gt;. Someone opens an issue, and it sits in the backlog for six weeks because it isn't a production bug, only a developer-experience gap.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Month 6: The workaround.&lt;/strong&gt; The seed file has broken twice in two months, so a senior developer wraps it in a script:&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;#!/bin/bash&lt;/span&gt;
&lt;span class="c"&gt;# run-seed.sh — "best effort" seeding&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="c"&gt;# just kidding&lt;/span&gt;
psql &lt;span class="nv"&gt;$DATABASE_URL&lt;/span&gt; &amp;lt; seed.sql 2&amp;gt;/dev/null &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Seed had errors (this is normal)"&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;|| echo&lt;/code&gt; is doing a lot of heavy lifting there. "This is normal" is doing even more.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Month 9: Abandonment.&lt;/strong&gt; The README still says "Run &lt;code&gt;./run-seed.sh&lt;/code&gt; to populate your local database," but new developers who try it watch it fail silently on half the tables, ask in Slack, and hear back "I just use the staging database" or "I manually insert what I need." The seed file is effectively dead. It still exists in the repo, since deleting it would mean acknowledging the problem and fixing it would mean taking on ongoing commitment nobody wants, so it just sits there, a monument to good intentions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Month 12: The zombie.&lt;/strong&gt; A new developer finds the seed file, spends two hours fixing it for the current schema, opens a PR, and the cycle begins again.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Do Seed Files Always Drift From the Schema?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Your schema changes constantly; your seed file stays static.&lt;/strong&gt; That's the root of every drift problem below.&lt;/p&gt;

&lt;p&gt;Consider a typical sprint. One developer adds a &lt;code&gt;phone_number&lt;/code&gt; column to &lt;code&gt;users&lt;/code&gt;. Another creates a &lt;code&gt;user_preferences&lt;/code&gt; table with a foreign key back to &lt;code&gt;users&lt;/code&gt;, while a third changes &lt;code&gt;orders.status&lt;/code&gt; from a text field to an enum type, and a fourth adds a check constraint requiring &lt;code&gt;orders.total&lt;/code&gt; to stay positive.&lt;/p&gt;

&lt;p&gt;Each of these changes is small, each migration gets tested, and each PR gets reviewed, but none of them touch the seed file, since it isn't anyone's job to. The seed file isn't part of the feature, it isn't in the test suite, and it isn't in the CI pipeline either, or if it once was, it got removed six months ago for breaking the build too often.&lt;/p&gt;

&lt;p&gt;The result is that the seed file drifts from the schema roughly in proportion to how fast your team ships schema-changing features, so the more productive the team is, the faster the seed file turns useless. &lt;a href="https://seedfa.st/docs/seed-your-database" rel="noopener noreferrer"&gt;Seedfast&lt;/a&gt; flips that relationship, so schema velocity stops being the enemy, and the faster the team ships migrations, the less there is left to maintain.&lt;/p&gt;

&lt;h2&gt;
  
  
  Nobody Owns It
&lt;/h2&gt;

&lt;p&gt;This is the human problem underneath the technical one. Who is responsible for &lt;code&gt;seed.sql&lt;/code&gt;?&lt;/p&gt;

&lt;p&gt;The developer who wrote it moved to another team long ago; the developer who added the new column is too busy shipping features to maintain test infrastructure; the tech lead already has forty other things to worry about; and DevOps treats it as application-level data rather than infrastructure, so it isn't their job either.&lt;/p&gt;

&lt;p&gt;Seed files are communal property, and communal property is everyone's responsibility and therefore nobody's. The same thing that happens to shared kitchen spaces in offices happens to seed files in repos, a slow, inevitable decay until someone snaps and does a deep clean. Except with seed files, nobody snaps; they just route around the damage.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Don't Migrations Update Seed Data Automatically?
&lt;/h2&gt;

&lt;p&gt;There's a deeper structural issue underneath this one. Migrations transform schema forward in time, while seed files stay frozen in the past, and your migration system knows how to get from schema version 47 to version 48 without having any idea how to update the test data that was valid at 47 into something still valid at 48.&lt;/p&gt;

&lt;p&gt;Some teams try to solve this by running seed files through the migration system, seeding at version 1 and then migrating up, which works fine until the first breaking migration, usually the third or fourth one, and from there you'd need to version your seed files alongside your migrations, maintaining parallel histories of schema changes and data changes that nobody keeps up for long.&lt;/p&gt;

&lt;h2&gt;
  
  
  Seed File Maintenance Across ORMs: Same Problem, Different Syntax
&lt;/h2&gt;

&lt;p&gt;Switching to a "proper" ORM seeder does not make the maintenance problem go away; it just moves the work to a different file. Every mainstream stack hits the same wall:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Rails (&lt;code&gt;db/seeds.rb&lt;/code&gt;).&lt;/strong&gt; Use &lt;code&gt;find_or_create_by!&lt;/code&gt; to stay &lt;a href="https://seedfa.st/blog/database-seeding" rel="noopener noreferrer"&gt;safe to re-run&lt;/a&gt;, split by model, keep seeds environment-aware. Organization improves; the columns are still hard-coded, so a &lt;code&gt;NOT NULL&lt;/code&gt; migration still breaks &lt;code&gt;bin/rails db:seed&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prisma (&lt;code&gt;prisma/seed.ts&lt;/code&gt;).&lt;/strong&gt; Recent Prisma versions decoupled seeding from &lt;code&gt;prisma migrate dev&lt;/code&gt; and &lt;code&gt;prisma migrate reset&lt;/code&gt;, so you now run &lt;code&gt;npx prisma db seed&lt;/code&gt; explicitly (&lt;a href="https://www.prisma.io/docs/orm/prisma-migrate/workflows/seeding" rel="noopener noreferrer"&gt;Prisma seeding docs&lt;/a&gt;). Adding a required field still breaks &lt;code&gt;prisma.user.create({ data: { ... } })&lt;/code&gt;, just at compile time instead of runtime, but the manual fix is the same.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Django (fixtures vs factories).&lt;/strong&gt; &lt;code&gt;loaddata&lt;/code&gt; fixtures break on most schema changes; &lt;code&gt;factory_boy&lt;/code&gt; generates rows from the live model, which is why guides have been recommending factories over fixtures for a decade (&lt;a href="https://www.caktusgroup.com/blog/2013/07/17/factory-boy-alternative-django-testing-fixtures/" rel="noopener noreferrer"&gt;Caktus Group, 2013&lt;/a&gt;). Factories help, but every new field is a patch to the factory code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Laravel (&lt;code&gt;DatabaseSeeder&lt;/code&gt; + model factories).&lt;/strong&gt; &lt;code&gt;User::factory()-&amp;gt;has(Post::factory()-&amp;gt;count(3))-&amp;gt;create()&lt;/code&gt; is composable, until &lt;code&gt;posts&lt;/code&gt; gains a &lt;code&gt;NOT NULL&lt;/code&gt; column and every seeder throws a &lt;code&gt;QueryException&lt;/code&gt;. The &lt;a href="https://laravel.com/docs/seeding" rel="noopener noreferrer"&gt;Laravel docs&lt;/a&gt; recommend keeping seeders deterministic and in CI; that recommendation is what creates the PR backlog when schemas move.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The pattern is the same across all four. &lt;strong&gt;The syntax differs, but you are still writing a file that describes the shape of your data by hand.&lt;/strong&gt; Every schema change forces a corresponding edit. For a side-by-side comparison of these tools and seven other seeding approaches, see &lt;a href="https://seedfa.st/blog/database-seeder" rel="noopener noreferrer"&gt;Database Seeder: 7 Tools Compared&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Hidden Cost of Seed File Maintenance
&lt;/h2&gt;

&lt;p&gt;The seed file seems like a small thing, just a convenience file for local development, but a broken one does more damage than you'd think; seed file maintenance is the kind of work that doesn't show up in any sprint plan yet eats engineering time everywhere.&lt;/p&gt;

&lt;h3&gt;
  
  
  Onboarding Delay
&lt;/h3&gt;

&lt;p&gt;A new developer joins the team, and the README says to clone the repo, run migrations, and run the seed file, but the seed file fails. Not knowing whether the failure is expected, whether their local setup is wrong, or whether they did something out of order, they spend an hour debugging before asking for help, and a senior developer spends another 30 minutes walking them through the workaround.&lt;/p&gt;

&lt;p&gt;Multiply this by every new developer, every quarter, then multiply again by the morale cost, since the new person's first experience with the codebase is discovering that the documented setup doesn't work, which is not a great first impression of your engineering culture.&lt;/p&gt;

&lt;h3&gt;
  
  
  Broken Local Development
&lt;/h3&gt;

&lt;p&gt;Without working seed data, local development means staring at empty states. The dashboard flashes "No data found," list views sit empty, searches return nothing, and graph components render nothing but a flat line.&lt;/p&gt;

&lt;p&gt;Developers start creating data manually through the UI, which takes ten minutes every time they reset their database. Or they stop resetting their database, which means their local state diverges from everyone else's. Or they just develop against staging, which has its own problems (shared state, slow connections, risk of interfering with QA).&lt;/p&gt;

&lt;p&gt;The empty local database is a productivity drain that's hard to quantify because it's spread across every developer, every day, in small increments, five minutes spent creating a test user here, ten minutes spent setting up an order with the right status there, twenty minutes spent building the exact data configuration a new feature needs to test, and it all adds up to hours per developer, every week.&lt;/p&gt;

&lt;h3&gt;
  
  
  CI Failures
&lt;/h3&gt;

&lt;p&gt;If your CI pipeline includes a seeding step (it should), a broken seed file means broken builds, and teams typically pick one of three options.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix the seed file every time it breaks.&lt;/strong&gt; This works, but it means someone is on permanent seed-file duty, patching SQL after every migration.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Remove the seeding step from CI.&lt;/strong&gt; This is what most teams actually do. The CI pipeline now tests against an empty database, which misses entire categories of &lt;a href="https://seedfa.st/blog/small-data-big-lies" rel="noopener noreferrer"&gt;bugs that only surface at realistic data volumes&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Make the seeding step non-fatal.&lt;/strong&gt; The &lt;code&gt;|| true&lt;/code&gt; approach. The seed runs, fails halfway, inserts data into some tables but not others, and the test suite runs against an inconsistent partial dataset. This is arguably worse than an empty database, because the failures are intermittent and hard to diagnose.&lt;/p&gt;

&lt;h3&gt;
  
  
  The "Just Comment It Out" Culture
&lt;/h3&gt;

&lt;p&gt;The most corrosive effect of a broken seed file is cultural. When developers learn that the seed file is unreliable, they develop a reflexive distrust of all shared data tooling. Suggestions to invest in better seeding infrastructure are met with "we tried that, it didn't work." Proposals for data-dependent integration tests are rejected with "those will just break when the seed file drifts."&lt;/p&gt;

&lt;p&gt;The broken seed file becomes a learned helplessness that prevents the team from investing in the thing they actually need.&lt;/p&gt;

&lt;p&gt;The way out is structural, not motivational. If your team has been burned enough times to distrust shared seed tooling, the fix is to stop shipping a tool that requires trust in the first place. Seedfast regenerates straight from the schema's current state instead of asking a human to keep a file in sync, so there's nothing left for the team to lose faith in.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 5-Minute seed.sql Health Check
&lt;/h2&gt;

&lt;p&gt;Before you decide what to do next, run a quick audit. This script spins up a fresh database, applies your migrations, runs your seed file, and reports exactly how broken it is. For deeper guidance on &lt;em&gt;writing&lt;/em&gt; a Postgres seed script that survives migrations, see &lt;a href="https://seedfa.st/blog/postgres-seed-script" rel="noopener noreferrer"&gt;Postgres Seed Script: Build One That Lasts&lt;/a&gt;. Copy this into &lt;code&gt;scripts/audit-seed.sh&lt;/code&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="c"&gt;# audit-seed.sh — find the drift between seed.sql and your current schema.&lt;/span&gt;
&lt;span class="c"&gt;# Usage: ./audit-seed.sh path/to/seed.sql&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-uo&lt;/span&gt; pipefail

&lt;span class="nv"&gt;SEED_FILE&lt;/span&gt;&lt;span class="o"&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;1&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="nv"&gt;seed&lt;/span&gt;&lt;span class="p"&gt;.sql&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nv"&gt;DB&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"seed_audit_&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%s&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="c"&gt;# 1. Fresh database, latest migrations applied.&lt;/span&gt;
createdb &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DB&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;trap&lt;/span&gt; &lt;span class="s1"&gt;'dropdb --if-exists "$DB"'&lt;/span&gt; EXIT

&lt;span class="c"&gt;# Replace with your project's migration runner:&lt;/span&gt;
&lt;span class="c"&gt;# Prisma: DATABASE_URL=postgres:///$DB npx prisma migrate deploy&lt;/span&gt;
&lt;span class="c"&gt;# Rails: DATABASE_URL=postgres:///$DB bin/rails db:migrate&lt;/span&gt;
&lt;span class="c"&gt;# Django: DATABASE_URL=postgres:///$DB python manage.py migrate&lt;/span&gt;
&lt;span class="c"&gt;# Raw SQL: psql "$DB" -v ON_ERROR_STOP=1 -f path/to/schema.sql&lt;/span&gt;

&lt;span class="c"&gt;# 2. Run the seed, capture every error (default ON_ERROR_STOP=off lets us collect them all).&lt;/span&gt;
&lt;span class="nv"&gt;ERRORS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;psql &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DB&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SEED_FILE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; 2&amp;gt;&amp;amp;1 &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s2"&gt;"ERROR|psql:.*: ERROR"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;# 3. Check which tables ended up empty despite being referenced in the seed.&lt;/span&gt;
&lt;span class="nv"&gt;REFERENCED_TABLES&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-iE&lt;/span&gt; &lt;span class="s2"&gt;"INSERT INTO ([a-z_][a-z0-9_]*)"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SEED_FILE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&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/.*INSERT INTO ([a-z_][a-z0-9_]*).*/\1/'&lt;/span&gt; | &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"=== seed.sql health report ==="&lt;/span&gt;
&lt;span class="nb"&gt;echo
&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;-z&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ERRORS&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;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"No SQL errors."&lt;/span&gt;
&lt;span class="k"&gt;else
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"SQL errors found:"&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ERRORS&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;fi
&lt;/span&gt;&lt;span class="nb"&gt;echo
echo&lt;/span&gt; &lt;span class="s2"&gt;"Tables referenced by the seed file and their row counts after running:"&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;t &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nv"&gt;$REFERENCED_TABLES&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;&lt;span class="nv"&gt;COUNT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;psql &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DB&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-Atc&lt;/span&gt; &lt;span class="s2"&gt;"SELECT count(*) FROM &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="nv"&gt;$t&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; 2&amp;gt;/dev/null &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"MISSING"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s2"&gt;" %-30s %s&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$t&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$COUNT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three things to look for in the output:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Error lines.&lt;/strong&gt; Each one is a migration your seed file has not caught up with.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tables with 0 rows.&lt;/strong&gt; The &lt;code&gt;INSERT&lt;/code&gt; succeeded syntactically but every row was rejected by a constraint you forgot about.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;MISSING&lt;/code&gt; tables.&lt;/strong&gt; The seed file references a table that no longer exists (it was renamed, merged, or dropped).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Anything more than zero in any of those categories is drift. If this is your first time running the check, expect the report to be longer than you want it to be.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Keeping seed.sql Alive Actually Costs Your Team
&lt;/h2&gt;

&lt;p&gt;The strongest argument for a static seed file is "it's just a file, how expensive can it be?" Turn it into a number.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Maintenance cost per month, as a formula.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cost = (minutes_per_break * breaks_per_migration * migrations_per_month)
     + (new_dev_onboarding_minutes * new_devs_per_month)
     + (minutes_per_dev_per_day_on_empty_db * devs * broken_seed_days_per_month)

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These numbers are illustrative; flip any input toward your actual team and the total moves. For a 10-person team that ships ~12 migrations a month, hires one engineer a quarter, and has a seed file that breaks on roughly one in three migrations:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Input&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Minutes to diagnose + fix one break&lt;/td&gt;
&lt;td&gt;45&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Break rate per migration&lt;/td&gt;
&lt;td&gt;0.33&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Migrations per month&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Onboarding time lost to broken seed&lt;/td&gt;
&lt;td&gt;90 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;New devs per month&lt;/td&gt;
&lt;td&gt;0.33&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Minutes lost per dev per day to empty-db workarounds (on days the seed is broken)&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Broken-seed days per month (≈ one in three of 20 working days)&lt;/td&gt;
&lt;td&gt;~8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Developers&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That comes out to roughly &lt;strong&gt;~178 minutes fixing breaks + ~30 minutes onboarding friction + ~800 minutes of empty-database workarounds&lt;/strong&gt; per month, about 17 hours, more than two full engineering days, spent on a file that was supposed to save time. Both levers move that total. Reset the local DB less often, say twice a week instead of daily, and the dev-day-loss term shrinks; ship fewer schema-changing migrations and the break-rate term shrinks too. The exact figure matters less than the fact that it's never zero and that it scales with how much schema work your team ships.&lt;/p&gt;

&lt;p&gt;Once you have a number, "just keep the seed file up to date" stops looking like a discipline problem and starts looking like a tax. Seedfast replaces that variable line item with a predictable subscription that does not scale with how often your schema changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Seed File Maintenance Coping Mechanisms
&lt;/h2&gt;

&lt;p&gt;Teams develop creative ways to live with broken seed files, but all of them are worse than fixing the root cause, because seed file maintenance is structural, not behavioral, and these patterns route around the structure instead of changing it.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Optional Seed
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## Local Setup&lt;/span&gt;
&lt;span class="p"&gt;1.&lt;/span&gt; Run &lt;span class="sb"&gt;`make migrate`&lt;/span&gt;
&lt;span class="p"&gt;2.&lt;/span&gt; (Optional) Run &lt;span class="sb"&gt;`make seed`&lt;/span&gt; to populate test data

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When the seed step is "optional," it means "broken." Nobody makes a working tool optional, and you don't see &lt;code&gt;(Optional) Run the compiler&lt;/code&gt; in setup docs; the word "optional" is a signal that the team knows the tool doesn't work reliably and has decided to make that someone else's problem.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Try-Catch Wrapper
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# seed.py
&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;table&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;users&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;orders&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;products&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;categories&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;run_sql&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;seed_&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;.sql&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Warning: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; seed failed (&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;), continuing...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every error gets swallowed, so half the tables succeed and half don't, and the developer has no way to know which half. The local database ends up with users but no orders, products but no categories, and the app technically runs even though half its features are untestable. Nobody investigates the warnings, because there are always warnings.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Versioned Seed
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;seeds/
  v1_initial.sql
  v2_add_roles.sql
  v3_add_addresses.sql
  v4_rename_products.sql
  v5_add_preferences.sql

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the most disciplined approach, and it's also the most labor-intensive, since every migration that affects seeded tables requires a corresponding seed update. In practice, this means the developer writing the migration now has two files to update and test (the migration itself and the seed delta). The &lt;a href="https://neon.com/blog/how-to-maintain-seed-data" rel="noopener noreferrer"&gt;Neon team's guide to maintaining seed data&lt;/a&gt; lays out this approach carefully (version the file, automate execution, keep it safe to re-run), and it is the best you can do with a static file. It also describes roughly one full-time responsibility you did not have before, and compliance drops rapidly after the first month. If your database is hosted on Neon, their branching feature offers an alternative path (instant schema-and-data snapshots per branch), but that approach is platform-specific and does not help the vast majority of teams running Postgres elsewhere. The platform-agnostic alternative is to stop versioning seed deltas entirely. Seedfast regenerates FK-valid rows from a scope string against the schema as it exists this sprint, so there is no &lt;code&gt;v6_*.sql&lt;/code&gt; file to write next sprint and no parallel history to maintain.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Per-Developer Seed
&lt;/h3&gt;

&lt;p&gt;Eventually, developers start maintaining their own personal seed files, each tailored to the features they happen to be working on, none of them complete, and none of them compatible with anyone else's. The team now ends up with N different versions of local state, where N is the number of developers.&lt;/p&gt;

&lt;p&gt;"Works on my machine" takes on a new meaning when every machine has different data.&lt;/p&gt;

&lt;p&gt;The team-by-team divergence is the moment most teams realize the file isn't recoverable. Seedfast replaces the per-developer sprawl with one command that pulls directly from the schema in front of it, so every developer gets the same shape of data because the schema, not a hand-edited file, is the source of truth.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Fundamental Problem
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;All of these failures trace back to the same mismatch between bulk data that stays static and a schema that never stops moving.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A seed file is a snapshot, capturing the shape of your data at a single point in time, and the moment your schema evolves, which it does constantly because that's what healthy software projects do, the snapshot goes stale.&lt;/p&gt;

&lt;p&gt;This isn't a discipline problem, and no amount of "just keeping the seed file up to date" fixes it, any more than "just checking your watch more often" fixes clock drift. The problem is structural, since you're using a static artifact to describe a moving target.&lt;/p&gt;

&lt;p&gt;Instead of writing a better seed file, the fix is to regenerate the bulk data on each run from the schema itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Do You Maintain Seed Files Without Maintaining Them?
&lt;/h2&gt;

&lt;p&gt;What if your seeder read your current schema every time it ran?&lt;/p&gt;

&lt;p&gt;This isn't a file that was written six months ago, a snapshot that assumed the &lt;code&gt;products&lt;/code&gt; table still exists, or a script that hardcodes column names; Seedfast reads the actual, current, live schema on each invocation, every column, constraint, foreign key, and enum that exists right now, at this moment. Describe what you want in plain English; Seedfast generates FK-valid rows that match the schema as it stands. This is the core idea behind &lt;a href="https://seedfa.st/blog/test-data-generation" rel="noopener noreferrer"&gt;schema-aware test data generation&lt;/a&gt;, and it's why nothing here is left for a human to keep in sync.&lt;/p&gt;

&lt;p&gt;Install once, then run it from any project directory.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; seedfast
&lt;span class="c"&gt;# or: brew install argon-it/tap/seedfast&lt;/span&gt;
&lt;span class="c"&gt;# or use it without installing: npx seedfast seed --scope "..."&lt;/span&gt;

seedfast connect &lt;span class="c"&gt;# paste your DATABASE_URL when prompted&lt;/span&gt;
seedfast seed &lt;span class="nt"&gt;--scope&lt;/span&gt; &lt;span class="s2"&gt;"seed realistic data for all tables"&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. There's no SQL file to maintain for bulk development data, no new columns to add to the seed after migrations, and no foreign keys to wire up manually. Seedfast connects to your database, matches the schema's current shape, and generates data that fits. Reference rows your tests assert on by literal value (admin accounts, country codes, feature flags) stay in a small hand-written &lt;code&gt;seed.sql&lt;/code&gt;; only the bulk development data gets regenerated.&lt;/p&gt;

&lt;p&gt;When a migration adds a &lt;code&gt;NOT NULL&lt;/code&gt; column next week, Seedfast sees it the next time it runs; when a table gets renamed, it uses the new name; when a foreign key gets added, it fills the column with a valid reference on the next run; and when an enum type gains a new value, it shows up in the distribution too, because there's no static file sitting between the schema and the data to fall out of sync.&lt;/p&gt;

&lt;h3&gt;
  
  
  Scope Instead of SQL
&lt;/h3&gt;

&lt;p&gt;Instead of writing SQL inserts, you describe what you need in plain English.&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;# Instead of maintaining 500 lines of INSERT statements&lt;/span&gt;
seedfast seed &lt;span class="nt"&gt;--scope&lt;/span&gt; &lt;span class="s2"&gt;"seed 1,000 users with orders, payments, and support tickets"&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Seedfast checks your schema, proposes a plan, and seeds. The scope description works today and will work next month, because it references concepts ("users with orders") rather than column names &lt;code&gt;(user_id INTEGER NOT NULL REFERENCES users(id))&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;When your schema changes, the same scope produces different data that matches the new schema, because the command and the intent stay the same; only the output adapts automatically.&lt;/p&gt;

&lt;h3&gt;
  
  
  What This Looks Like in Practice
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Before (the seed.sql lifecycle):&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Developer writes &lt;code&gt;seed.sql&lt;/code&gt; (2 hours)&lt;/li&gt;
&lt;li&gt;Works for 3 weeks&lt;/li&gt;
&lt;li&gt;Migration breaks it (5 minutes to discover, 30 minutes to fix)&lt;/li&gt;
&lt;li&gt;Works for 2 weeks&lt;/li&gt;
&lt;li&gt;Another migration breaks it (someone files an issue)&lt;/li&gt;
&lt;li&gt;Issue sits in backlog for 6 weeks&lt;/li&gt;
&lt;li&gt;New developer fixes it (1 hour)&lt;/li&gt;
&lt;li&gt;Works for 1 week&lt;/li&gt;
&lt;li&gt;Two migrations land in the same sprint, seed file breaks in multiple places&lt;/li&gt;
&lt;li&gt;Someone wraps it in &lt;code&gt;|| true&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Team stops using it&lt;/li&gt;
&lt;li&gt;Repeat from step 7 every few months&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Cumulative time: dozens of hours per year. Effective uptime: maybe 40%.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;After (seedfast):&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="c"&gt;# In your README&lt;/span&gt;
seedfast seed &lt;span class="nt"&gt;--scope&lt;/span&gt; &lt;span class="s2"&gt;"seed realistic data for all tables"&lt;/span&gt;

&lt;span class="c"&gt;# In CI&lt;/span&gt;
seedfast seed &lt;span class="nt"&gt;--scope&lt;/span&gt; &lt;span class="s2"&gt;"seed 1,000 users with orders"&lt;/span&gt; &lt;span class="nt"&gt;--output&lt;/span&gt; plain

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is no step 2 through 12. The command works after every migration because it lines up with the tables as they exist at that moment. There is no SQL file to patch and no error-swallowing wrapper script to maintain, so the seed-after-each-migration treadmill goes away.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does It Work on Real-World Schemas?
&lt;/h3&gt;

&lt;p&gt;Toy schemas with &lt;code&gt;users → orders → products&lt;/code&gt; are easy. The interesting question is what happens when Seedfast meets the kind of schema that has been running in production for three years. Here's a short, honest support matrix:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Foreign keys (single-column and composite):&lt;/strong&gt; Every row comes out valid and connected, with no manual insert ordering required. Self-referential and looped foreign-key relationships are handled without special-casing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enum types and check constraints:&lt;/strong&gt; Recognized and respected, with generated values drawn from the enum's members and passing the &lt;code&gt;CHECK&lt;/code&gt; predicate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Generated columns (&lt;code&gt;GENERATED ALWAYS AS ...&lt;/code&gt;):&lt;/strong&gt; Skipped on insert; the database computes them. Stored generated columns work the same way.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Partitioned tables (range/list/hash):&lt;/strong&gt; Inserts go through the parent table; Postgres routes rows to the correct partition. No special configuration needed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;citext&lt;/code&gt;, &lt;code&gt;jsonb&lt;/code&gt;, &lt;code&gt;uuid&lt;/code&gt;, &lt;code&gt;tsvector&lt;/code&gt;, &lt;code&gt;numeric(10,2)&lt;/code&gt;, geometric types:&lt;/strong&gt; Supported via Postgres's type system; Seedfast generates type-correct values.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Triggers with side effects (audit rows, denormalized counts):&lt;/strong&gt; They fire as written. If a trigger does something you don't want during seeding, disable it for the seed run the same way you would for a &lt;code&gt;pg_restore&lt;/code&gt;, using &lt;code&gt;ALTER TABLE ... DISABLE TRIGGER USER&lt;/code&gt;, then run the seed and re-enable it afterward.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Views, materialized views, and functions:&lt;/strong&gt; Not seeded directly (they don't hold data). Materialized views you may want to &lt;code&gt;REFRESH&lt;/code&gt; after seeding.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-schema databases:&lt;/strong&gt; Tables in non-&lt;code&gt;public&lt;/code&gt; schemas are read and seeded as long as your &lt;code&gt;DATABASE_URL&lt;/code&gt; user has access.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your schema has something exotic that's not listed (extension-defined types, custom domains, row-level security policies that conflict with the seeding role), &lt;code&gt;seedfast doctor&lt;/code&gt; will report what it can and can't handle before you commit to a run. The honest answer to "does this work on my real schema" is that in most cases it does, in some cases with one extra setup step, and &lt;code&gt;seedfast plan&lt;/code&gt; will tell you exactly which before any rows are written.&lt;/p&gt;

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

&lt;p&gt;The seed file in CI is where the pain compounds, because CI failures block everyone. If you want the full walk-through for GitHub Actions and GitLab, the &lt;a href="https://seedfa.st/docs/cicd-database-seeding" rel="noopener noreferrer"&gt;CI/CD database seeding guide&lt;/a&gt; covers exit codes and artifact patterns. Here's the short version.&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="c1"&gt;# Before: fragile, breaks every few sprints&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;Seed test database&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;psql $DATABASE_URL &amp;lt; seed.sql&lt;/span&gt; &lt;span class="c1"&gt;# fingers crossed&lt;/span&gt;

&lt;span class="c1"&gt;# After: reads current schema every time&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;Seed test database&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;seedfast seed --scope "seed 5,000 users with orders and payments" --output plain&lt;/span&gt;
  &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;SEEDFAST_API_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.SEEDFAST_API_KEY }}&lt;/span&gt;
    &lt;span class="na"&gt;SEEDFAST_DSN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.SEEDFAST_DSN }}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;--scope&lt;/code&gt; flag makes it non-interactive. To make the run idempotent, point each CI job at a fresh ephemeral database (e.g., a Postgres service container), since Seedfast appends rows rather than replacing them, so re-running against an already-populated database stacks more data on top. If you need a clean slate without recreating the DB, truncate the affected tables first.&lt;/p&gt;

&lt;h3&gt;
  
  
  For Onboarding
&lt;/h3&gt;

&lt;p&gt;The before-and-after for new developers is dramatic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Before:&lt;/strong&gt; Clone repo. Run migrations. Run seed. Seed fails. Ask Slack. Wait for response. Get workaround. Apply workaround. Half the data loads. Manually create the rest. Time: 1-3 hours.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;After:&lt;/strong&gt; Clone repo. Run migrations. Run &lt;code&gt;seedfast seed&lt;/code&gt;, review the proposed plan, hit Y. Time: typically 2 minutes on a small schema.&lt;/p&gt;

&lt;p&gt;There's no debugging, no Slack, no workarounds anymore. The database comes populated with realistic data that matches the current schema, and the new developer sees a working dashboard on day one instead of an empty state with a TODO comment.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Uncomfortable Truth About Seed File Maintenance
&lt;/h2&gt;

&lt;p&gt;Your &lt;code&gt;seed.sql&lt;/code&gt; isn't broken because your team is lazy, but because the premise itself is flawed, since asking a static file to keep up with a dynamic schema means asking for perpetual maintenance, and perpetual maintenance of non-production tooling is exactly the kind of work that gets deprioritized, postponed, and eventually abandoned.&lt;/p&gt;

&lt;p&gt;The teams that have working seed files are the ones spending real engineering time on seed file maintenance, time that could be spent on features, on tests, on the product instead. Maintaining seed files is not a valuable use of engineering time; it's a tax you pay because the tool requires it. The answer to &lt;em&gt;how to maintain seed files&lt;/em&gt; in any production codebase, eventually, is to stop trying. At enterprise scale the math only gets worse. See &lt;a href="https://seedfa.st/blog/enterprise-database-test-data" rel="noopener noreferrer"&gt;enterprise database test data&lt;/a&gt; for what compliance and volume add to the bill, and &lt;a href="https://seedfa.st/blog/staging-without-prod-data" rel="noopener noreferrer"&gt;staging without prod data&lt;/a&gt; for why the regulated audience cannot just copy production into dev.&lt;/p&gt;

&lt;p&gt;Stop paying the tax by moving the bulk data out of &lt;code&gt;seed.sql&lt;/code&gt;, keeping only the reference rows your tests assert on by literal value, and letting Seedfast regenerate the rest to match the schema you actually have. &lt;a href="https://seedfa.st/" rel="noopener noreferrer"&gt;Try Seedfast free →&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it on your seed file
&lt;/h2&gt;

&lt;p&gt;If your &lt;code&gt;seed.sql&lt;/code&gt; is the file nobody wants to own, point &lt;a href="https://seedfa.st/docs/seed-your-database" rel="noopener noreferrer"&gt;Seedfast&lt;/a&gt; at the same database and let it generate the bulk data instead, matched to whichever tables are actually there when it runs. Reference rows your tests assert on by literal value, a handful of admin accounts or role names, stay in a short &lt;code&gt;seed.sql&lt;/code&gt;. Everything else (the 500 users, 2,000 orders, the relational bulk that breaks every migration) gets regenerated on each run to match it.&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;# Try without installing — runs the latest CLI in this shell only:&lt;/span&gt;
npx seedfast connect
npx seedfast seed &lt;span class="nt"&gt;--scope&lt;/span&gt; &lt;span class="s2"&gt;"1,000 users with orders, payments, and support tickets"&lt;/span&gt;

&lt;span class="c"&gt;# Or install once for repeated use:&lt;/span&gt;
&lt;span class="c"&gt;# npm install -g seedfast&lt;/span&gt;
&lt;span class="c"&gt;# brew install argon-it/tap/seedfast&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There's no file to update after the next migration and no factory code to keep in sync. The schema is the source of truth, and Seedfast checks it fresh on each invocation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;On privacy.&lt;/strong&gt; Seedfast's synthetic data generation does not require samples of your existing rows; the pipeline reads schema metadata (table and column definitions, types, FK relationships, constraints) and the scope text you provide. Your &lt;code&gt;DATABASE_URL&lt;/code&gt; stays on your machine; the CLI connects to your database directly, and the password is never transmitted to Seedfast's servers. Note that &lt;code&gt;DEFAULT&lt;/code&gt; expressions, &lt;code&gt;CHECK&lt;/code&gt; constraints, and enum values are part of schema metadata and are transmitted as written; if any of those contain sensitive literals, treat them accordingly. See &lt;a href="https://seedfa.st/docs/how-it-works" rel="noopener noreferrer"&gt;how it works&lt;/a&gt; for the full data-handling breakdown.&lt;/p&gt;

&lt;p&gt;The free plan covers connecting and running the first seed, and small schemas typically fit inside its monthly credits. See &lt;a href="https://seedfa.st/pricing" rel="noopener noreferrer"&gt;pricing&lt;/a&gt; for current terms, or read the full walkthrough in the &lt;a href="https://seedfa.st/docs/seed-your-database" rel="noopener noreferrer"&gt;getting started guide&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Why does my seed.sql break every time someone runs a migration?
&lt;/h3&gt;

&lt;p&gt;Because a seed file is a snapshot of the schema at the moment someone wrote it, while a migration is, by definition, a change to that schema, and any migration that adds a &lt;code&gt;NOT NULL&lt;/code&gt; column, a foreign key, a check constraint, or renames a table will break any &lt;code&gt;INSERT&lt;/code&gt; statement that doesn't already account for it. Rather than remembering to update the seed after every migration, the fix is to generate seed data from the current schema on every run, so there's nothing left to remember.&lt;/p&gt;

&lt;h3&gt;
  
  
  What's the difference between a seed file and a migration?
&lt;/h3&gt;

&lt;p&gt;A migration changes the &lt;strong&gt;shape&lt;/strong&gt; of your database (&lt;code&gt;CREATE TABLE&lt;/code&gt;, &lt;code&gt;ALTER TABLE ADD COLUMN&lt;/code&gt;, &lt;code&gt;DROP INDEX&lt;/code&gt;), while a seed file inserts &lt;strong&gt;rows&lt;/strong&gt; into the shape that migrations created. Migrations are versioned, ordered, and applied once per environment; seed data is rerun on demand. The two get conflated because both can be &lt;code&gt;.sql&lt;/code&gt; files, but migrations answer "what does my schema look like?" while seeds answer "what data lives in it?" Mixing rows into migrations is a common anti-pattern; it makes migrations non-replayable across environments.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I make my seed file idempotent (seed only once safely)?
&lt;/h3&gt;

&lt;p&gt;Idempotency means the second run produces the same final state as the first. Raw SQL reaches for &lt;code&gt;INSERT ... ON CONFLICT DO NOTHING&lt;/code&gt; against a unique key, Rails uses &lt;code&gt;find_or_create_by!&lt;/code&gt;, Prisma uses &lt;code&gt;upsert&lt;/code&gt; with &lt;code&gt;where&lt;/code&gt;/&lt;code&gt;create&lt;/code&gt;/&lt;code&gt;update&lt;/code&gt;, and Django has the &lt;code&gt;update_or_create&lt;/code&gt; queryset method. None of these solve drift; they only protect against duplicate rows when the same script runs twice. A seed that is safe to re-run still breaks the day a migration adds a &lt;code&gt;NOT NULL&lt;/code&gt; column the script doesn't know about.&lt;/p&gt;

&lt;h3&gt;
  
  
  What changed in Prisma 7 — why was --skip-seed removed?
&lt;/h3&gt;

&lt;p&gt;Prisma 7 decoupled seeding from migration commands, so &lt;code&gt;prisma migrate dev&lt;/code&gt; and &lt;code&gt;prisma migrate reset&lt;/code&gt; no longer auto-run the seed, and the &lt;code&gt;--skip-seed&lt;/code&gt; flag was retired along with it because there is no automatic seed run left to skip. To seed, you call &lt;code&gt;npx prisma db seed&lt;/code&gt; explicitly (&lt;a href="https://www.prisma.io/docs/orm/prisma-migrate/workflows/seeding" rel="noopener noreferrer"&gt;Prisma seeding docs&lt;/a&gt;). The change is cleaner (migrations and seeding are now separate concerns), but it does not fix the underlying drift problem. Your &lt;code&gt;prisma/seed.ts&lt;/code&gt; still hard-codes column shapes, and a required field added to the model still breaks &lt;code&gt;db seed&lt;/code&gt; until you patch the file.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I just version my seed files alongside migrations?
&lt;/h3&gt;

&lt;p&gt;You can, and some teams do. This is the most disciplined manual approach, adding a corresponding seed delta for every migration that changes a seeded table, one that reshapes the existing fixtures to match. In practice, compliance collapses within a few weeks, because every PR now costs a second file to update, and reviewers stop catching it. It works for very small teams with slow-moving schemas, but it doesn't scale.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is the difference between fixtures, seed files, and factories?
&lt;/h3&gt;

&lt;p&gt;Fixtures and seed files are both static artifacts, JSON/YAML/SQL that describe specific rows and are loaded verbatim, and they share the same drift problem. Factories (Factory Bot, &lt;code&gt;factory_boy&lt;/code&gt;, Laravel model factories) are code that &lt;em&gt;generates&lt;/em&gt; rows at test time using the current model definitions, so renaming a field is a compile-time error instead of a silent runtime failure. Factories are a clear upgrade over fixtures, but you still hand-write one per model, so schema changes still force edits, just in Ruby/Python/PHP instead of SQL.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does Prisma's or Rails' built-in seeding solve seed file maintenance?
&lt;/h3&gt;

&lt;p&gt;No. Prisma 7 actually removed automatic seeding from &lt;code&gt;prisma migrate dev&lt;/code&gt; and &lt;code&gt;prisma migrate reset&lt;/code&gt; (you now call &lt;code&gt;npx prisma db seed&lt;/code&gt; explicitly), and Rails has always kept &lt;code&gt;db:seed&lt;/code&gt; as a manual step. Both frameworks give you a nice place to put seed code; neither frees you from writing out the shape of your data by hand. Built-in seeders move where seed file maintenance happens, not whether it happens. See the &lt;a href="https://seedfa.st/blog/database-seeding" rel="noopener noreferrer"&gt;database seeding overview&lt;/a&gt; for how ORM seeders fit into the broader picture.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is "just make the seed safe to re-run" enough?
&lt;/h3&gt;

&lt;p&gt;Safe-to-re-run (&lt;code&gt;find_or_create_by!&lt;/code&gt;, &lt;code&gt;ON CONFLICT DO NOTHING&lt;/code&gt;, &lt;code&gt;upsert&lt;/code&gt;) is necessary but not sufficient. It prevents the &lt;em&gt;second&lt;/em&gt; run from failing; it does nothing about the first run failing because a new required column was added. You still need to edit the seed to match every schema change. Re-runnability is table stakes, not a solution.&lt;/p&gt;

&lt;h3&gt;
  
  
  When should I still keep a hand-written seed file?
&lt;/h3&gt;

&lt;p&gt;Two cases keep seed file maintenance worthwhile. The first is &lt;strong&gt;reference data that ships to production&lt;/strong&gt; (country codes, role names, default feature flags), which is part of your application's contract and belongs in migrations or a narrowly scoped seed step. The second is &lt;strong&gt;specific named rows your tests assert on by ID&lt;/strong&gt; , a handful of rows clearly separated from bulk development data. Everything else (the 500 users, 2,000 orders, 10,000 line items you need for realistic local dev and CI) should be generated on demand from the current schema.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I migrate from seed.sql to schema-aware generation without a big-bang rewrite?
&lt;/h3&gt;

&lt;p&gt;Leave the existing file in place. Add &lt;code&gt;seedfast seed --scope "..."&lt;/code&gt; to the end of your setup script so new developers get schema-fresh data. Once everyone is on the new command, drop the &lt;code&gt;.sql&lt;/code&gt; file. There's no flag day and no coordination needed; the two approaches coexist fine because they both write to the same tables. The &lt;a href="https://seedfa.st/docs/seed-your-database" rel="noopener noreferrer"&gt;getting started guide&lt;/a&gt; walks through the first run.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does &lt;code&gt;seedfast seed&lt;/code&gt; replace &lt;code&gt;npx prisma db seed&lt;/code&gt;?
&lt;/h3&gt;

&lt;p&gt;It can, and for most teams it should. In &lt;code&gt;package.json&lt;/code&gt; you can either swap the &lt;code&gt;"prisma": { "seed": "..." }&lt;/code&gt; command to call &lt;code&gt;seedfast seed --scope "..."&lt;/code&gt;, or leave &lt;code&gt;prisma db seed&lt;/code&gt; for a small handful of hand-written reference rows and point developers at &lt;code&gt;seedfast seed&lt;/code&gt; for bulk data. Seedfast connects to the same &lt;code&gt;DATABASE_URL&lt;/code&gt; Prisma uses, so there is no separate connection config. The Seedfast CLI doesn't care which ORM you use; it works from the schema in the live database directly.&lt;/p&gt;

&lt;h3&gt;
  
  
  What does Seedfast actually send to its AI provider — is my row data exposed?
&lt;/h3&gt;

&lt;p&gt;Nothing leaves your machine except the shape of your database and the words you type describing what you want seeded. On the schema side, that means table and column names, data types, nullability, length limits, foreign keys, and constraint definitions, plus your natural-language scope text; the AI provider generates synthetic rows from that shape, never from samples of your actual data.&lt;/p&gt;

&lt;p&gt;Your &lt;code&gt;DATABASE_URL&lt;/code&gt; and its password never make the trip; the CLI uses them locally to connect to your database and nothing more. One caveat is worth knowing, though. Schema metadata includes &lt;code&gt;DEFAULT&lt;/code&gt; expressions, &lt;code&gt;CHECK&lt;/code&gt; constraints, and enum values exactly as written, so a sensitive literal buried in a default or a check constraint gets transmitted along with everything else, and the same goes for whatever you put in your scope text. For the complete breakdown, see &lt;a href="https://seedfa.st/docs/how-it-works" rel="noopener noreferrer"&gt;how it works&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related guides
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://seedfa.st/docs/seed-your-database" rel="noopener noreferrer"&gt;Getting Started&lt;/a&gt; walks through installing Seedfast and seeding your first database&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://seedfa.st/docs/cicd-database-seeding" rel="noopener noreferrer"&gt;CI/CD Database Seeding&lt;/a&gt; automates seeding so nobody maintains scripts manually&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://seedfa.st/blog/database-seeding" rel="noopener noreferrer"&gt;Database Seeding: Methods and Best Practices&lt;/a&gt; covers how seeding works across SQL, ORMs, and generators&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://seedfa.st/blog/database-seeder" rel="noopener noreferrer"&gt;Database Seeder: 7 Tools Compared&lt;/a&gt; picks the right seeder tool for your stack, with a quick syntax reference per ORM&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://seedfa.st/blog/supabase-db-seed" rel="noopener noreferrer"&gt;Supabase config.toml [db.seed] and sql_paths&lt;/a&gt; configures Supabase's seed pipeline through &lt;code&gt;supabase/config.toml&lt;/code&gt; and the &lt;code&gt;[db.seed]&lt;/code&gt; block, plus what happens on preview branches&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://seedfa.st/blog/test-data-generation" rel="noopener noreferrer"&gt;Test Data Generation&lt;/a&gt; explains schema-aware data generation that adapts to migrations&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://seedfa.st/blog/e2e-test-fixtures" rel="noopener noreferrer"&gt;E2E Test Fixtures&lt;/a&gt; shows how to replace JSON/YAML fixtures with generated data&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://seedfa.st/blog/seed-file-maintenance" rel="noopener noreferrer"&gt;seedfa.st&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>sql</category>
      <category>database</category>
      <category>devops</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
